1pub mod git_traversal;
2
3use crate::{
4 ProjectEnvironment, ProjectItem, ProjectPath,
5 buffer_store::{BufferStore, BufferStoreEvent},
6 worktree_store::{WorktreeStore, WorktreeStoreEvent},
7};
8use anyhow::{Context as _, Result, anyhow, bail};
9use askpass::AskPassDelegate;
10use buffer_diff::{BufferDiff, BufferDiffEvent};
11use client::ProjectId;
12use collections::HashMap;
13use fs::Fs;
14use futures::{
15 FutureExt as _, StreamExt as _,
16 channel::{mpsc, oneshot},
17 future::{self, OptionFuture, Shared},
18};
19use git::{
20 BuildPermalinkParams, GitHostingProviderRegistry,
21 blame::Blame,
22 parse_git_remote_url,
23 repository::{
24 Branch, CommitDetails, DiffType, GitRepository, GitRepositoryCheckpoint, PushOptions,
25 Remote, RemoteCommandOutput, RepoPath, ResetMode,
26 },
27 status::FileStatus,
28};
29use gpui::{
30 App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,
31 WeakEntity,
32};
33use language::{
34 Buffer, BufferEvent, Language, LanguageRegistry,
35 proto::{deserialize_version, serialize_version},
36};
37use parking_lot::Mutex;
38use rpc::{
39 AnyProtoClient, TypedEnvelope,
40 proto::{self, FromProto, SSH_PROJECT_ID, ToProto, git_reset},
41};
42use serde::Deserialize;
43use settings::WorktreeId;
44use std::{
45 collections::{VecDeque, hash_map},
46 future::Future,
47 ops::Range,
48 path::{Path, PathBuf},
49 sync::Arc,
50};
51use sum_tree::TreeSet;
52use text::BufferId;
53use util::{ResultExt, debug_panic, maybe};
54use worktree::{
55 File, PathKey, ProjectEntryId, RepositoryEntry, StatusEntry, UpdatedGitRepositoriesSet,
56 Worktree, proto_to_branch,
57};
58
59pub struct GitStore {
60 state: GitStoreState,
61 buffer_store: Entity<BufferStore>,
62 worktree_store: Entity<WorktreeStore>,
63 repositories: HashMap<ProjectEntryId, Entity<Repository>>,
64 active_repo_id: Option<ProjectEntryId>,
65 #[allow(clippy::type_complexity)]
66 loading_diffs:
67 HashMap<(BufferId, DiffKind), Shared<Task<Result<Entity<BufferDiff>, Arc<anyhow::Error>>>>>,
68 diffs: HashMap<BufferId, Entity<BufferDiffState>>,
69 update_sender: mpsc::UnboundedSender<GitJob>,
70 shared_diffs: HashMap<proto::PeerId, HashMap<BufferId, SharedDiffs>>,
71 _subscriptions: [Subscription; 2],
72}
73
74#[derive(Default)]
75struct SharedDiffs {
76 unstaged: Option<Entity<BufferDiff>>,
77 uncommitted: Option<Entity<BufferDiff>>,
78}
79
80#[derive(Default)]
81struct BufferDiffState {
82 unstaged_diff: Option<WeakEntity<BufferDiff>>,
83 uncommitted_diff: Option<WeakEntity<BufferDiff>>,
84 recalculate_diff_task: Option<Task<Result<()>>>,
85 language: Option<Arc<Language>>,
86 language_registry: Option<Arc<LanguageRegistry>>,
87 diff_updated_futures: Vec<oneshot::Sender<()>>,
88 hunk_staging_operation_count: usize,
89
90 head_text: Option<Arc<String>>,
91 index_text: Option<Arc<String>>,
92 head_changed: bool,
93 index_changed: bool,
94 language_changed: bool,
95}
96
97#[derive(Clone, Debug)]
98enum DiffBasesChange {
99 SetIndex(Option<String>),
100 SetHead(Option<String>),
101 SetEach {
102 index: Option<String>,
103 head: Option<String>,
104 },
105 SetBoth(Option<String>),
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
109enum DiffKind {
110 Unstaged,
111 Uncommitted,
112}
113
114enum GitStoreState {
115 Local {
116 downstream_client: Option<LocalDownstreamState>,
117 environment: Entity<ProjectEnvironment>,
118 fs: Arc<dyn Fs>,
119 },
120 Ssh {
121 upstream_client: AnyProtoClient,
122 upstream_project_id: ProjectId,
123 downstream_client: Option<(AnyProtoClient, ProjectId)>,
124 environment: Entity<ProjectEnvironment>,
125 },
126 Remote {
127 upstream_client: AnyProtoClient,
128 project_id: ProjectId,
129 },
130}
131
132enum DownstreamUpdate {
133 UpdateRepository(RepositoryEntry),
134 RemoveRepository(ProjectEntryId),
135}
136
137struct LocalDownstreamState {
138 client: AnyProtoClient,
139 project_id: ProjectId,
140 updates_tx: mpsc::UnboundedSender<DownstreamUpdate>,
141 _task: Task<Result<()>>,
142}
143
144#[derive(Clone)]
145pub struct GitStoreCheckpoint {
146 checkpoints_by_work_dir_abs_path: HashMap<PathBuf, GitRepositoryCheckpoint>,
147}
148
149pub struct Repository {
150 pub repository_entry: RepositoryEntry,
151 pub merge_message: Option<String>,
152 pub completed_scan_id: usize,
153 commit_message_buffer: Option<Entity<Buffer>>,
154 git_store: WeakEntity<GitStore>,
155 project_environment: Option<WeakEntity<ProjectEnvironment>>,
156 pub worktree_id: Option<WorktreeId>,
157 state: RepositoryState,
158 job_sender: mpsc::UnboundedSender<GitJob>,
159 askpass_delegates: Arc<Mutex<HashMap<u64, AskPassDelegate>>>,
160 latest_askpass_id: u64,
161}
162
163#[derive(Clone)]
164enum RepositoryState {
165 Local(Arc<dyn GitRepository>),
166 Remote {
167 project_id: ProjectId,
168 client: AnyProtoClient,
169 work_directory_id: ProjectEntryId,
170 },
171}
172
173#[derive(Debug)]
174pub enum GitEvent {
175 ActiveRepositoryChanged,
176 FileSystemUpdated,
177 GitStateUpdated,
178 IndexWriteError(anyhow::Error),
179}
180
181struct GitJob {
182 job: Box<dyn FnOnce(&mut AsyncApp) -> Task<()>>,
183 key: Option<GitJobKey>,
184}
185
186#[derive(PartialEq, Eq)]
187enum GitJobKey {
188 WriteIndex(RepoPath),
189 BatchReadIndex(ProjectEntryId),
190}
191
192impl EventEmitter<GitEvent> for GitStore {}
193
194impl GitStore {
195 pub fn local(
196 worktree_store: &Entity<WorktreeStore>,
197 buffer_store: Entity<BufferStore>,
198 environment: Entity<ProjectEnvironment>,
199 fs: Arc<dyn Fs>,
200 cx: &mut Context<Self>,
201 ) -> Self {
202 Self::new(
203 worktree_store.clone(),
204 buffer_store,
205 GitStoreState::Local {
206 downstream_client: None,
207 environment,
208 fs,
209 },
210 cx,
211 )
212 }
213
214 pub fn remote(
215 worktree_store: &Entity<WorktreeStore>,
216 buffer_store: Entity<BufferStore>,
217 upstream_client: AnyProtoClient,
218 project_id: ProjectId,
219 cx: &mut Context<Self>,
220 ) -> Self {
221 Self::new(
222 worktree_store.clone(),
223 buffer_store,
224 GitStoreState::Remote {
225 upstream_client,
226 project_id,
227 },
228 cx,
229 )
230 }
231
232 pub fn ssh(
233 worktree_store: &Entity<WorktreeStore>,
234 buffer_store: Entity<BufferStore>,
235 environment: Entity<ProjectEnvironment>,
236 upstream_client: AnyProtoClient,
237 cx: &mut Context<Self>,
238 ) -> Self {
239 Self::new(
240 worktree_store.clone(),
241 buffer_store,
242 GitStoreState::Ssh {
243 upstream_client,
244 upstream_project_id: ProjectId(SSH_PROJECT_ID),
245 downstream_client: None,
246 environment,
247 },
248 cx,
249 )
250 }
251
252 fn new(
253 worktree_store: Entity<WorktreeStore>,
254 buffer_store: Entity<BufferStore>,
255 state: GitStoreState,
256 cx: &mut Context<Self>,
257 ) -> Self {
258 let update_sender = Self::spawn_git_worker(cx);
259 let _subscriptions = [
260 cx.subscribe(&worktree_store, Self::on_worktree_store_event),
261 cx.subscribe(&buffer_store, Self::on_buffer_store_event),
262 ];
263
264 GitStore {
265 state,
266 buffer_store,
267 worktree_store,
268 repositories: HashMap::default(),
269 active_repo_id: None,
270 update_sender,
271 _subscriptions,
272 loading_diffs: HashMap::default(),
273 shared_diffs: HashMap::default(),
274 diffs: HashMap::default(),
275 }
276 }
277
278 pub fn init(client: &AnyProtoClient) {
279 client.add_entity_request_handler(Self::handle_get_remotes);
280 client.add_entity_request_handler(Self::handle_get_branches);
281 client.add_entity_request_handler(Self::handle_change_branch);
282 client.add_entity_request_handler(Self::handle_create_branch);
283 client.add_entity_request_handler(Self::handle_git_init);
284 client.add_entity_request_handler(Self::handle_push);
285 client.add_entity_request_handler(Self::handle_pull);
286 client.add_entity_request_handler(Self::handle_fetch);
287 client.add_entity_request_handler(Self::handle_stage);
288 client.add_entity_request_handler(Self::handle_unstage);
289 client.add_entity_request_handler(Self::handle_commit);
290 client.add_entity_request_handler(Self::handle_reset);
291 client.add_entity_request_handler(Self::handle_show);
292 client.add_entity_request_handler(Self::handle_checkout_files);
293 client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
294 client.add_entity_request_handler(Self::handle_set_index_text);
295 client.add_entity_request_handler(Self::handle_askpass);
296 client.add_entity_request_handler(Self::handle_check_for_pushed_commits);
297 client.add_entity_request_handler(Self::handle_git_diff);
298 client.add_entity_request_handler(Self::handle_open_unstaged_diff);
299 client.add_entity_request_handler(Self::handle_open_uncommitted_diff);
300 client.add_entity_message_handler(Self::handle_update_diff_bases);
301 client.add_entity_request_handler(Self::handle_get_permalink_to_line);
302 client.add_entity_request_handler(Self::handle_blame_buffer);
303 client.add_entity_message_handler(Self::handle_update_repository);
304 client.add_entity_message_handler(Self::handle_remove_repository);
305 }
306
307 pub fn is_local(&self) -> bool {
308 matches!(self.state, GitStoreState::Local { .. })
309 }
310
311 pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) {
312 match &mut self.state {
313 GitStoreState::Ssh {
314 downstream_client, ..
315 } => {
316 for repo in self.repositories.values() {
317 client
318 .send(repo.read(cx).repository_entry.initial_update(project_id))
319 .log_err();
320 }
321 *downstream_client = Some((client, ProjectId(project_id)));
322 }
323 GitStoreState::Local {
324 downstream_client, ..
325 } => {
326 let mut snapshots = HashMap::default();
327 let (updates_tx, mut updates_rx) = mpsc::unbounded();
328 for repo in self.repositories.values() {
329 updates_tx
330 .unbounded_send(DownstreamUpdate::UpdateRepository(
331 repo.read(cx).repository_entry.clone(),
332 ))
333 .ok();
334 }
335 *downstream_client = Some(LocalDownstreamState {
336 client: client.clone(),
337 project_id: ProjectId(project_id),
338 updates_tx,
339 _task: cx.spawn(async move |this, cx| {
340 cx.background_spawn(async move {
341 while let Some(update) = updates_rx.next().await {
342 match update {
343 DownstreamUpdate::UpdateRepository(snapshot) => {
344 if let Some(old_snapshot) =
345 snapshots.get_mut(&snapshot.work_directory_id)
346 {
347 let update =
348 snapshot.build_update(old_snapshot, project_id);
349 *old_snapshot = snapshot;
350 client.send(update)?;
351 } else {
352 let update = snapshot.initial_update(project_id);
353 client.send(update)?;
354 snapshots.insert(snapshot.work_directory_id, snapshot);
355 }
356 }
357 DownstreamUpdate::RemoveRepository(id) => {
358 client.send(proto::RemoveRepository {
359 project_id,
360 id: id.to_proto(),
361 })?;
362 }
363 }
364 }
365 anyhow::Ok(())
366 })
367 .await
368 .ok();
369 this.update(cx, |this, _| {
370 if let GitStoreState::Local {
371 downstream_client, ..
372 } = &mut this.state
373 {
374 downstream_client.take();
375 } else {
376 unreachable!("unshared called on remote store");
377 }
378 })
379 }),
380 });
381 }
382 GitStoreState::Remote { .. } => {
383 debug_panic!("shared called on remote store");
384 }
385 }
386 }
387
388 pub fn unshared(&mut self, _cx: &mut Context<Self>) {
389 match &mut self.state {
390 GitStoreState::Local {
391 downstream_client, ..
392 } => {
393 downstream_client.take();
394 }
395 GitStoreState::Ssh {
396 downstream_client, ..
397 } => {
398 downstream_client.take();
399 }
400 GitStoreState::Remote { .. } => {
401 debug_panic!("unshared called on remote store");
402 }
403 }
404 self.shared_diffs.clear();
405 }
406
407 pub(crate) fn forget_shared_diffs_for(&mut self, peer_id: &proto::PeerId) {
408 self.shared_diffs.remove(peer_id);
409 }
410
411 pub fn active_repository(&self) -> Option<Entity<Repository>> {
412 self.active_repo_id
413 .as_ref()
414 .map(|id| self.repositories[&id].clone())
415 }
416
417 pub fn open_unstaged_diff(
418 &mut self,
419 buffer: Entity<Buffer>,
420 cx: &mut Context<Self>,
421 ) -> Task<Result<Entity<BufferDiff>>> {
422 let buffer_id = buffer.read(cx).remote_id();
423 if let Some(diff_state) = self.diffs.get(&buffer_id) {
424 if let Some(unstaged_diff) = diff_state
425 .read(cx)
426 .unstaged_diff
427 .as_ref()
428 .and_then(|weak| weak.upgrade())
429 {
430 if let Some(task) =
431 diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
432 {
433 return cx.background_executor().spawn(async move {
434 task.await?;
435 Ok(unstaged_diff)
436 });
437 }
438 return Task::ready(Ok(unstaged_diff));
439 }
440 }
441
442 let task = match self.loading_diffs.entry((buffer_id, DiffKind::Unstaged)) {
443 hash_map::Entry::Occupied(e) => e.get().clone(),
444 hash_map::Entry::Vacant(entry) => {
445 let staged_text = self.state.load_staged_text(&buffer, &self.buffer_store, cx);
446 entry
447 .insert(
448 cx.spawn(async move |this, cx| {
449 Self::open_diff_internal(
450 this,
451 DiffKind::Unstaged,
452 staged_text.await.map(DiffBasesChange::SetIndex),
453 buffer,
454 cx,
455 )
456 .await
457 .map_err(Arc::new)
458 })
459 .shared(),
460 )
461 .clone()
462 }
463 };
464
465 cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
466 }
467
468 pub fn open_uncommitted_diff(
469 &mut self,
470 buffer: Entity<Buffer>,
471 cx: &mut Context<Self>,
472 ) -> Task<Result<Entity<BufferDiff>>> {
473 let buffer_id = buffer.read(cx).remote_id();
474
475 if let Some(diff_state) = self.diffs.get(&buffer_id) {
476 if let Some(uncommitted_diff) = diff_state
477 .read(cx)
478 .uncommitted_diff
479 .as_ref()
480 .and_then(|weak| weak.upgrade())
481 {
482 if let Some(task) =
483 diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation())
484 {
485 return cx.background_executor().spawn(async move {
486 task.await?;
487 Ok(uncommitted_diff)
488 });
489 }
490 return Task::ready(Ok(uncommitted_diff));
491 }
492 }
493
494 let task = match self.loading_diffs.entry((buffer_id, DiffKind::Uncommitted)) {
495 hash_map::Entry::Occupied(e) => e.get().clone(),
496 hash_map::Entry::Vacant(entry) => {
497 let changes = self
498 .state
499 .load_committed_text(&buffer, &self.buffer_store, cx);
500
501 entry
502 .insert(
503 cx.spawn(async move |this, cx| {
504 Self::open_diff_internal(
505 this,
506 DiffKind::Uncommitted,
507 changes.await,
508 buffer,
509 cx,
510 )
511 .await
512 .map_err(Arc::new)
513 })
514 .shared(),
515 )
516 .clone()
517 }
518 };
519
520 cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) })
521 }
522
523 async fn open_diff_internal(
524 this: WeakEntity<Self>,
525 kind: DiffKind,
526 texts: Result<DiffBasesChange>,
527 buffer_entity: Entity<Buffer>,
528 cx: &mut AsyncApp,
529 ) -> Result<Entity<BufferDiff>> {
530 let diff_bases_change = match texts {
531 Err(e) => {
532 this.update(cx, |this, cx| {
533 let buffer = buffer_entity.read(cx);
534 let buffer_id = buffer.remote_id();
535 this.loading_diffs.remove(&(buffer_id, kind));
536 })?;
537 return Err(e);
538 }
539 Ok(change) => change,
540 };
541
542 this.update(cx, |this, cx| {
543 let buffer = buffer_entity.read(cx);
544 let buffer_id = buffer.remote_id();
545 let language = buffer.language().cloned();
546 let language_registry = buffer.language_registry();
547 let text_snapshot = buffer.text_snapshot();
548 this.loading_diffs.remove(&(buffer_id, kind));
549
550 let diff_state = this
551 .diffs
552 .entry(buffer_id)
553 .or_insert_with(|| cx.new(|_| BufferDiffState::default()));
554
555 let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
556
557 cx.subscribe(&diff, Self::on_buffer_diff_event).detach();
558 diff_state.update(cx, |diff_state, cx| {
559 diff_state.language = language;
560 diff_state.language_registry = language_registry;
561
562 match kind {
563 DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()),
564 DiffKind::Uncommitted => {
565 let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() {
566 diff
567 } else {
568 let unstaged_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx));
569 diff_state.unstaged_diff = Some(unstaged_diff.downgrade());
570 unstaged_diff
571 };
572
573 diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff));
574 diff_state.uncommitted_diff = Some(diff.downgrade())
575 }
576 }
577
578 let rx = diff_state.diff_bases_changed(text_snapshot, diff_bases_change, 0, cx);
579
580 anyhow::Ok(async move {
581 rx.await.ok();
582 Ok(diff)
583 })
584 })
585 })??
586 .await
587 }
588
589 pub fn get_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Option<Entity<BufferDiff>> {
590 let diff_state = self.diffs.get(&buffer_id)?;
591 diff_state.read(cx).unstaged_diff.as_ref()?.upgrade()
592 }
593
594 pub fn get_uncommitted_diff(
595 &self,
596 buffer_id: BufferId,
597 cx: &App,
598 ) -> Option<Entity<BufferDiff>> {
599 let diff_state = self.diffs.get(&buffer_id)?;
600 diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade()
601 }
602
603 pub fn project_path_git_status(
604 &self,
605 project_path: &ProjectPath,
606 cx: &App,
607 ) -> Option<FileStatus> {
608 let (repo, repo_path) = self.repository_and_path_for_project_path(project_path, cx)?;
609 Some(
610 repo.read(cx)
611 .repository_entry
612 .status_for_path(&repo_path)?
613 .status,
614 )
615 }
616
617 pub fn checkpoint(&self, cx: &App) -> Task<Result<GitStoreCheckpoint>> {
618 let mut work_directory_abs_paths = Vec::new();
619 let mut checkpoints = Vec::new();
620 for repository in self.repositories.values() {
621 let repository = repository.read(cx);
622 work_directory_abs_paths
623 .push(repository.repository_entry.work_directory_abs_path.clone());
624 checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?));
625 }
626
627 cx.background_executor().spawn(async move {
628 let checkpoints = future::try_join_all(checkpoints).await?;
629 Ok(GitStoreCheckpoint {
630 checkpoints_by_work_dir_abs_path: work_directory_abs_paths
631 .into_iter()
632 .zip(checkpoints)
633 .collect(),
634 })
635 })
636 }
637
638 pub fn restore_checkpoint(&self, checkpoint: GitStoreCheckpoint, cx: &App) -> Task<Result<()>> {
639 let repositories_by_work_dir_abs_path = self
640 .repositories
641 .values()
642 .map(|repo| {
643 (
644 repo.read(cx)
645 .repository_entry
646 .work_directory_abs_path
647 .clone(),
648 repo,
649 )
650 })
651 .collect::<HashMap<_, _>>();
652
653 let mut tasks = Vec::new();
654 for (work_dir_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path {
655 if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) {
656 let restore = repository.read(cx).restore_checkpoint(checkpoint);
657 tasks.push(async move { restore.await? });
658 }
659 }
660 cx.background_spawn(async move {
661 future::try_join_all(tasks).await?;
662 Ok(())
663 })
664 }
665
666 /// Compares two checkpoints, returning true if they are equal.
667 pub fn compare_checkpoints(
668 &self,
669 left: GitStoreCheckpoint,
670 mut right: GitStoreCheckpoint,
671 cx: &App,
672 ) -> Task<Result<bool>> {
673 let repositories_by_work_dir_abs_path = self
674 .repositories
675 .values()
676 .map(|repo| {
677 (
678 repo.read(cx)
679 .repository_entry
680 .work_directory_abs_path
681 .clone(),
682 repo,
683 )
684 })
685 .collect::<HashMap<_, _>>();
686
687 let mut tasks = Vec::new();
688 for (work_dir_abs_path, left_checkpoint) in left.checkpoints_by_work_dir_abs_path {
689 if let Some(right_checkpoint) = right
690 .checkpoints_by_work_dir_abs_path
691 .remove(&work_dir_abs_path)
692 {
693 if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path)
694 {
695 let compare = repository
696 .read(cx)
697 .compare_checkpoints(left_checkpoint, right_checkpoint);
698 tasks.push(async move { compare.await? });
699 }
700 } else {
701 return Task::ready(Ok(false));
702 }
703 }
704 cx.background_spawn(async move {
705 Ok(future::try_join_all(tasks)
706 .await?
707 .into_iter()
708 .all(|result| result))
709 })
710 }
711
712 pub fn delete_checkpoint(&self, checkpoint: GitStoreCheckpoint, cx: &App) -> Task<Result<()>> {
713 let repositories_by_work_directory_abs_path = self
714 .repositories
715 .values()
716 .map(|repo| {
717 (
718 repo.read(cx)
719 .repository_entry
720 .work_directory_abs_path
721 .clone(),
722 repo,
723 )
724 })
725 .collect::<HashMap<_, _>>();
726
727 let mut tasks = Vec::new();
728 for (work_dir_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path {
729 if let Some(repository) =
730 repositories_by_work_directory_abs_path.get(&work_dir_abs_path)
731 {
732 let delete = repository.read(cx).delete_checkpoint(checkpoint);
733 tasks.push(async move { delete.await? });
734 }
735 }
736 cx.background_spawn(async move {
737 future::try_join_all(tasks).await?;
738 Ok(())
739 })
740 }
741
742 /// Blames a buffer.
743 pub fn blame_buffer(
744 &self,
745 buffer: &Entity<Buffer>,
746 version: Option<clock::Global>,
747 cx: &App,
748 ) -> Task<Result<Option<Blame>>> {
749 let buffer = buffer.read(cx);
750 let Some(file) = File::from_dyn(buffer.file()) else {
751 return Task::ready(Err(anyhow!("buffer has no file")));
752 };
753
754 match file.worktree.clone().read(cx) {
755 Worktree::Local(worktree) => {
756 let worktree = worktree.snapshot();
757 let blame_params = maybe!({
758 let local_repo = match worktree.local_repo_containing_path(&file.path) {
759 Some(repo_for_path) => repo_for_path,
760 None => return Ok(None),
761 };
762
763 let relative_path = local_repo
764 .relativize(&file.path)
765 .context("failed to relativize buffer path")?;
766
767 let repo = local_repo.repo().clone();
768
769 let content = match version {
770 Some(version) => buffer.rope_for_version(&version).clone(),
771 None => buffer.as_rope().clone(),
772 };
773
774 anyhow::Ok(Some((repo, relative_path, content)))
775 });
776
777 cx.spawn(async move |_cx| {
778 let Some((repo, relative_path, content)) = blame_params? else {
779 return Ok(None);
780 };
781 repo.blame(relative_path.clone(), content)
782 .await
783 .with_context(|| format!("Failed to blame {:?}", relative_path.0))
784 .map(Some)
785 })
786 }
787 Worktree::Remote(worktree) => {
788 let buffer_id = buffer.remote_id();
789 let version = buffer.version();
790 let project_id = worktree.project_id();
791 let client = worktree.client();
792 cx.spawn(async move |_| {
793 let response = client
794 .request(proto::BlameBuffer {
795 project_id,
796 buffer_id: buffer_id.into(),
797 version: serialize_version(&version),
798 })
799 .await?;
800 Ok(deserialize_blame_buffer_response(response))
801 })
802 }
803 }
804 }
805
806 pub fn get_permalink_to_line(
807 &self,
808 buffer: &Entity<Buffer>,
809 selection: Range<u32>,
810 cx: &App,
811 ) -> Task<Result<url::Url>> {
812 let buffer = buffer.read(cx);
813 let Some(file) = File::from_dyn(buffer.file()) else {
814 return Task::ready(Err(anyhow!("buffer has no file")));
815 };
816
817 match file.worktree.read(cx) {
818 Worktree::Local(worktree) => {
819 let repository = self
820 .repository_and_path_for_project_path(
821 &(worktree.id(), file.path.clone()).into(),
822 cx,
823 )
824 .map(|(repository, _)| repository);
825 let Some((local_repo_entry, repo_entry)) = repository.and_then(|repository| {
826 let repository = repository.read(cx);
827 let repo_entry = repository.repository_entry.clone();
828 Some((worktree.get_local_repo(&repo_entry)?, repo_entry))
829 }) else {
830 // If we're not in a Git repo, check whether this is a Rust source
831 // file in the Cargo registry (presumably opened with go-to-definition
832 // from a normal Rust file). If so, we can put together a permalink
833 // using crate metadata.
834 if buffer
835 .language()
836 .is_none_or(|lang| lang.name() != "Rust".into())
837 {
838 return Task::ready(Err(anyhow!("no permalink available")));
839 }
840 let Some(file_path) = worktree.absolutize(&file.path).ok() else {
841 return Task::ready(Err(anyhow!("no permalink available")));
842 };
843 return cx.spawn(async move |cx| {
844 let provider_registry =
845 cx.update(GitHostingProviderRegistry::default_global)?;
846 get_permalink_in_rust_registry_src(provider_registry, file_path, selection)
847 .map_err(|_| anyhow!("no permalink available"))
848 });
849 };
850
851 let path = match local_repo_entry.relativize(&file.path) {
852 Ok(RepoPath(path)) => path,
853 Err(e) => return Task::ready(Err(e)),
854 };
855
856 let remote = repo_entry
857 .branch()
858 .and_then(|b| b.upstream.as_ref())
859 .and_then(|b| b.remote_name())
860 .unwrap_or("origin")
861 .to_string();
862
863 let repo = local_repo_entry.repo().clone();
864 cx.spawn(async move |cx| {
865 let origin_url = repo
866 .remote_url(&remote)
867 .ok_or_else(|| anyhow!("remote \"{remote}\" not found"))?;
868
869 let sha = repo
870 .head_sha()
871 .ok_or_else(|| anyhow!("failed to read HEAD SHA"))?;
872
873 let provider_registry =
874 cx.update(GitHostingProviderRegistry::default_global)?;
875
876 let (provider, remote) =
877 parse_git_remote_url(provider_registry, &origin_url)
878 .ok_or_else(|| anyhow!("failed to parse Git remote URL"))?;
879
880 let path = path
881 .to_str()
882 .ok_or_else(|| anyhow!("failed to convert path to string"))?;
883
884 Ok(provider.build_permalink(
885 remote,
886 BuildPermalinkParams {
887 sha: &sha,
888 path,
889 selection: Some(selection),
890 },
891 ))
892 })
893 }
894 Worktree::Remote(worktree) => {
895 let buffer_id = buffer.remote_id();
896 let project_id = worktree.project_id();
897 let client = worktree.client();
898 cx.spawn(async move |_| {
899 let response = client
900 .request(proto::GetPermalinkToLine {
901 project_id,
902 buffer_id: buffer_id.into(),
903 selection: Some(proto::Range {
904 start: selection.start as u64,
905 end: selection.end as u64,
906 }),
907 })
908 .await?;
909
910 url::Url::parse(&response.permalink).context("failed to parse permalink")
911 })
912 }
913 }
914 }
915
916 fn downstream_client(&self) -> Option<(AnyProtoClient, ProjectId)> {
917 match &self.state {
918 GitStoreState::Local {
919 downstream_client, ..
920 } => downstream_client
921 .as_ref()
922 .map(|state| (state.client.clone(), state.project_id)),
923 GitStoreState::Ssh {
924 downstream_client, ..
925 } => downstream_client.clone(),
926 GitStoreState::Remote { .. } => None,
927 }
928 }
929
930 fn upstream_client(&self) -> Option<AnyProtoClient> {
931 match &self.state {
932 GitStoreState::Local { .. } => None,
933 GitStoreState::Ssh {
934 upstream_client, ..
935 }
936 | GitStoreState::Remote {
937 upstream_client, ..
938 } => Some(upstream_client.clone()),
939 }
940 }
941
942 fn project_environment(&self) -> Option<Entity<ProjectEnvironment>> {
943 match &self.state {
944 GitStoreState::Local { environment, .. } => Some(environment.clone()),
945 GitStoreState::Ssh { environment, .. } => Some(environment.clone()),
946 GitStoreState::Remote { .. } => None,
947 }
948 }
949
950 fn project_id(&self) -> Option<ProjectId> {
951 match &self.state {
952 GitStoreState::Local { .. } => None,
953 GitStoreState::Ssh { .. } => Some(ProjectId(proto::SSH_PROJECT_ID)),
954 GitStoreState::Remote { project_id, .. } => Some(*project_id),
955 }
956 }
957
958 fn on_worktree_store_event(
959 &mut self,
960 worktree_store: Entity<WorktreeStore>,
961 event: &WorktreeStoreEvent,
962 cx: &mut Context<Self>,
963 ) {
964 match event {
965 WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => {
966 // We should only get this event for a local project.
967 self.update_repositories(&worktree_store, cx);
968 if self.is_local() {
969 if let Some(worktree) =
970 worktree_store.read(cx).worktree_for_id(*worktree_id, cx)
971 {
972 self.local_worktree_git_repos_changed(worktree, changed_repos, cx);
973 }
974 }
975 cx.emit(GitEvent::GitStateUpdated);
976 }
977 WorktreeStoreEvent::WorktreeAdded(_) => {}
978 _ => {
979 cx.emit(GitEvent::FileSystemUpdated);
980 }
981 }
982 }
983
984 fn update_repositories(
985 &mut self,
986 worktree_store: &Entity<WorktreeStore>,
987 cx: &mut Context<GitStore>,
988 ) {
989 let mut new_repositories = HashMap::default();
990 let git_store = cx.weak_entity();
991 worktree_store.update(cx, |worktree_store, cx| {
992 for worktree in worktree_store.worktrees() {
993 worktree.update(cx, |worktree, cx| {
994 let snapshot = worktree.snapshot();
995 for repo_entry in snapshot.repositories().iter() {
996 let git_repo_and_merge_message = worktree
997 .as_local()
998 .and_then(|local_worktree| local_worktree.get_local_repo(repo_entry))
999 .map(|local_repo| {
1000 (
1001 RepositoryState::Local(local_repo.repo().clone()),
1002 local_repo.merge_message.clone(),
1003 )
1004 })
1005 .or_else(|| {
1006 let git_repo = RepositoryState::Remote {
1007 project_id: self.project_id()?,
1008 client: self
1009 .upstream_client()
1010 .context("no upstream client")
1011 .log_err()?
1012 .clone(),
1013 work_directory_id: repo_entry.work_directory_id(),
1014 };
1015 Some((git_repo, None))
1016 });
1017
1018 let Some((git_repo, merge_message)) = git_repo_and_merge_message else {
1019 continue;
1020 };
1021
1022 let existing_repo = self
1023 .repositories
1024 .values()
1025 .find(|repo| repo.read(cx).id() == repo_entry.work_directory_id());
1026
1027 let repo = if let Some(existing_repo) = existing_repo {
1028 // Update the statuses and merge message but keep everything else.
1029 let existing_repo = existing_repo.clone();
1030 existing_repo.update(cx, |existing_repo, _| {
1031 existing_repo.repository_entry = repo_entry.clone();
1032 if matches!(git_repo, RepositoryState::Local { .. }) {
1033 existing_repo.merge_message = merge_message;
1034 existing_repo.completed_scan_id = worktree.completed_scan_id();
1035 }
1036 });
1037 existing_repo
1038 } else {
1039 cx.new(|_| Repository {
1040 worktree_id: Some(worktree.id()),
1041 project_environment: self
1042 .project_environment()
1043 .as_ref()
1044 .map(|env| env.downgrade()),
1045 git_store: git_store.clone(),
1046 askpass_delegates: Default::default(),
1047 latest_askpass_id: 0,
1048 repository_entry: repo_entry.clone(),
1049 job_sender: self.update_sender.clone(),
1050 merge_message,
1051 commit_message_buffer: None,
1052 completed_scan_id: worktree.completed_scan_id(),
1053 state: git_repo,
1054 })
1055 };
1056
1057 // TODO only send out messages for repository snapshots that have changed
1058 let snapshot = repo.read(cx).repository_entry.clone();
1059 if let GitStoreState::Local {
1060 downstream_client: Some(state),
1061 ..
1062 } = &self.state
1063 {
1064 state
1065 .updates_tx
1066 .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot))
1067 .ok();
1068 }
1069 new_repositories.insert(repo_entry.work_directory_id(), repo);
1070 self.repositories.remove(&repo_entry.work_directory_id());
1071 }
1072 })
1073 }
1074 });
1075
1076 if let GitStoreState::Local {
1077 downstream_client: Some(state),
1078 ..
1079 } = &self.state
1080 {
1081 for id in self.repositories.keys().cloned() {
1082 state
1083 .updates_tx
1084 .unbounded_send(DownstreamUpdate::RemoveRepository(id))
1085 .ok();
1086 }
1087 }
1088
1089 self.repositories = new_repositories;
1090 if let Some(id) = self.active_repo_id.as_ref() {
1091 if !self.repositories.contains_key(id) {
1092 self.active_repo_id = None;
1093 }
1094 } else if let Some(&first_id) = self.repositories.keys().next() {
1095 self.active_repo_id = Some(first_id);
1096 }
1097 }
1098
1099 fn on_buffer_store_event(
1100 &mut self,
1101 _: Entity<BufferStore>,
1102 event: &BufferStoreEvent,
1103 cx: &mut Context<Self>,
1104 ) {
1105 match event {
1106 BufferStoreEvent::BufferAdded(buffer) => {
1107 cx.subscribe(&buffer, |this, buffer, event, cx| {
1108 if let BufferEvent::LanguageChanged = event {
1109 let buffer_id = buffer.read(cx).remote_id();
1110 if let Some(diff_state) = this.diffs.get(&buffer_id) {
1111 diff_state.update(cx, |diff_state, cx| {
1112 diff_state.buffer_language_changed(buffer, cx);
1113 });
1114 }
1115 }
1116 })
1117 .detach();
1118 }
1119 BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id) => {
1120 if let Some(diffs) = self.shared_diffs.get_mut(peer_id) {
1121 diffs.remove(buffer_id);
1122 }
1123 }
1124 BufferStoreEvent::BufferDropped(buffer_id) => {
1125 self.diffs.remove(&buffer_id);
1126 for diffs in self.shared_diffs.values_mut() {
1127 diffs.remove(buffer_id);
1128 }
1129 }
1130
1131 _ => {}
1132 }
1133 }
1134
1135 pub fn recalculate_buffer_diffs(
1136 &mut self,
1137 buffers: Vec<Entity<Buffer>>,
1138 cx: &mut Context<Self>,
1139 ) -> impl Future<Output = ()> + use<> {
1140 let mut futures = Vec::new();
1141 for buffer in buffers {
1142 if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) {
1143 let buffer = buffer.read(cx).text_snapshot();
1144 futures.push(diff_state.update(cx, |diff_state, cx| {
1145 diff_state.recalculate_diffs(
1146 buffer,
1147 diff_state.hunk_staging_operation_count,
1148 cx,
1149 )
1150 }));
1151 }
1152 }
1153 async move {
1154 futures::future::join_all(futures).await;
1155 }
1156 }
1157
1158 fn on_buffer_diff_event(
1159 &mut self,
1160 diff: Entity<buffer_diff::BufferDiff>,
1161 event: &BufferDiffEvent,
1162 cx: &mut Context<Self>,
1163 ) {
1164 if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event {
1165 let buffer_id = diff.read(cx).buffer_id;
1166 if let Some(diff_state) = self.diffs.get(&buffer_id) {
1167 diff_state.update(cx, |diff_state, _| {
1168 diff_state.hunk_staging_operation_count += 1;
1169 });
1170 }
1171 if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) {
1172 let recv = repo.update(cx, |repo, cx| {
1173 log::debug!("updating index text for buffer {}", path.display());
1174 repo.spawn_set_index_text_job(
1175 path,
1176 new_index_text.as_ref().map(|rope| rope.to_string()),
1177 cx,
1178 )
1179 });
1180 let diff = diff.downgrade();
1181 cx.spawn(async move |this, cx| {
1182 if let Ok(Err(error)) = cx.background_spawn(recv).await {
1183 diff.update(cx, |diff, cx| {
1184 diff.clear_pending_hunks(cx);
1185 })
1186 .ok();
1187 this.update(cx, |_, cx| cx.emit(GitEvent::IndexWriteError(error)))
1188 .ok();
1189 }
1190 })
1191 .detach();
1192 }
1193 }
1194 }
1195
1196 fn local_worktree_git_repos_changed(
1197 &mut self,
1198 worktree: Entity<Worktree>,
1199 changed_repos: &UpdatedGitRepositoriesSet,
1200 cx: &mut Context<Self>,
1201 ) {
1202 debug_assert!(worktree.read(cx).is_local());
1203
1204 let Some(active_repo) = self.active_repository() else {
1205 log::error!("local worktree changed but we have no active repository");
1206 return;
1207 };
1208
1209 let mut diff_state_updates = HashMap::<ProjectEntryId, Vec<_>>::default();
1210 for (buffer_id, diff_state) in &self.diffs {
1211 let Some(buffer) = self.buffer_store.read(cx).get(*buffer_id) else {
1212 continue;
1213 };
1214 let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
1215 continue;
1216 };
1217 if file.worktree != worktree {
1218 continue;
1219 }
1220 let Some(repo_id) = changed_repos
1221 .iter()
1222 .map(|(entry, _)| entry.id)
1223 .find(|repo_id| self.repositories().contains_key(&repo_id))
1224 else {
1225 continue;
1226 };
1227
1228 let diff_state = diff_state.read(cx);
1229 let has_unstaged_diff = diff_state
1230 .unstaged_diff
1231 .as_ref()
1232 .is_some_and(|diff| diff.is_upgradable());
1233 let has_uncommitted_diff = diff_state
1234 .uncommitted_diff
1235 .as_ref()
1236 .is_some_and(|set| set.is_upgradable());
1237
1238 let update = (
1239 buffer,
1240 file.path.clone(),
1241 has_unstaged_diff.then(|| diff_state.index_text.clone()),
1242 has_uncommitted_diff.then(|| diff_state.head_text.clone()),
1243 diff_state.hunk_staging_operation_count,
1244 );
1245 diff_state_updates.entry(repo_id).or_default().push(update);
1246 }
1247
1248 if diff_state_updates.is_empty() {
1249 return;
1250 }
1251
1252 for (repo_id, repo_diff_state_updates) in diff_state_updates.into_iter() {
1253 let worktree = worktree.downgrade();
1254 let git_store = cx.weak_entity();
1255
1256 let _ = active_repo.read(cx).send_keyed_job(
1257 Some(GitJobKey::BatchReadIndex(repo_id)),
1258 |_, mut cx| async move {
1259 let snapshot = worktree.update(&mut cx, |tree, _| {
1260 tree.as_local().map(|local_tree| local_tree.snapshot())
1261 });
1262 let Ok(Some(snapshot)) = snapshot else {
1263 return;
1264 };
1265
1266 let mut diff_bases_changes_by_buffer = Vec::new();
1267 for (
1268 buffer,
1269 path,
1270 current_index_text,
1271 current_head_text,
1272 hunk_staging_operation_count,
1273 ) in &repo_diff_state_updates
1274 {
1275 let Some(local_repo) = snapshot.local_repo_containing_path(&path) else {
1276 continue;
1277 };
1278 let Some(relative_path) = local_repo.relativize(&path).ok() else {
1279 continue;
1280 };
1281
1282 log::debug!("reloading git state for buffer {}", path.display());
1283 let index_text = if current_index_text.is_some() {
1284 local_repo
1285 .repo()
1286 .load_index_text(relative_path.clone())
1287 .await
1288 } else {
1289 None
1290 };
1291 let head_text = if current_head_text.is_some() {
1292 local_repo.repo().load_committed_text(relative_path).await
1293 } else {
1294 None
1295 };
1296
1297 // Avoid triggering a diff update if the base text has not changed.
1298 if let Some((current_index, current_head)) =
1299 current_index_text.as_ref().zip(current_head_text.as_ref())
1300 {
1301 if current_index.as_deref() == index_text.as_ref()
1302 && current_head.as_deref() == head_text.as_ref()
1303 {
1304 continue;
1305 }
1306 }
1307
1308 let diff_bases_change =
1309 match (current_index_text.is_some(), current_head_text.is_some()) {
1310 (true, true) => Some(if index_text == head_text {
1311 DiffBasesChange::SetBoth(head_text)
1312 } else {
1313 DiffBasesChange::SetEach {
1314 index: index_text,
1315 head: head_text,
1316 }
1317 }),
1318 (true, false) => Some(DiffBasesChange::SetIndex(index_text)),
1319 (false, true) => Some(DiffBasesChange::SetHead(head_text)),
1320 (false, false) => None,
1321 };
1322
1323 diff_bases_changes_by_buffer.push((
1324 buffer,
1325 diff_bases_change,
1326 *hunk_staging_operation_count,
1327 ))
1328 }
1329
1330 git_store
1331 .update(&mut cx, |git_store, cx| {
1332 for (buffer, diff_bases_change, hunk_staging_operation_count) in
1333 diff_bases_changes_by_buffer
1334 {
1335 let Some(diff_state) =
1336 git_store.diffs.get(&buffer.read(cx).remote_id())
1337 else {
1338 continue;
1339 };
1340 let Some(diff_bases_change) = diff_bases_change else {
1341 continue;
1342 };
1343
1344 let downstream_client = git_store.downstream_client();
1345 diff_state.update(cx, |diff_state, cx| {
1346 use proto::update_diff_bases::Mode;
1347
1348 let buffer = buffer.read(cx);
1349 if let Some((client, project_id)) = downstream_client {
1350 let (staged_text, committed_text, mode) =
1351 match diff_bases_change.clone() {
1352 DiffBasesChange::SetIndex(index) => {
1353 (index, None, Mode::IndexOnly)
1354 }
1355 DiffBasesChange::SetHead(head) => {
1356 (None, head, Mode::HeadOnly)
1357 }
1358 DiffBasesChange::SetEach { index, head } => {
1359 (index, head, Mode::IndexAndHead)
1360 }
1361 DiffBasesChange::SetBoth(text) => {
1362 (None, text, Mode::IndexMatchesHead)
1363 }
1364 };
1365 let message = proto::UpdateDiffBases {
1366 project_id: project_id.to_proto(),
1367 buffer_id: buffer.remote_id().to_proto(),
1368 staged_text,
1369 committed_text,
1370 mode: mode as i32,
1371 };
1372
1373 client.send(message).log_err();
1374 }
1375
1376 let _ = diff_state.diff_bases_changed(
1377 buffer.text_snapshot(),
1378 diff_bases_change,
1379 hunk_staging_operation_count,
1380 cx,
1381 );
1382 });
1383 }
1384 })
1385 .ok();
1386 },
1387 );
1388 }
1389 }
1390
1391 pub fn repositories(&self) -> &HashMap<ProjectEntryId, Entity<Repository>> {
1392 &self.repositories
1393 }
1394
1395 pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option<FileStatus> {
1396 let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?;
1397 let status = repo.read(cx).repository_entry.status_for_path(&path)?;
1398 Some(status.status)
1399 }
1400
1401 pub fn repository_and_path_for_buffer_id(
1402 &self,
1403 buffer_id: BufferId,
1404 cx: &App,
1405 ) -> Option<(Entity<Repository>, RepoPath)> {
1406 let buffer = self.buffer_store.read(cx).get(buffer_id)?;
1407 let project_path = buffer.read(cx).project_path(cx)?;
1408 self.repository_and_path_for_project_path(&project_path, cx)
1409 }
1410
1411 pub fn repository_and_path_for_project_path(
1412 &self,
1413 path: &ProjectPath,
1414 cx: &App,
1415 ) -> Option<(Entity<Repository>, RepoPath)> {
1416 let abs_path = self.worktree_store.read(cx).absolutize(path, cx)?;
1417 self.repositories
1418 .values()
1419 .filter_map(|repo_handle| {
1420 let repo = repo_handle.read(cx);
1421 let relative_path = repo.repository_entry.relativize_abs_path(&abs_path)?;
1422 Some((repo_handle.clone(), relative_path))
1423 })
1424 .max_by_key(|(repo, _)| {
1425 repo.read(cx)
1426 .repository_entry
1427 .work_directory_abs_path
1428 .clone()
1429 })
1430 }
1431
1432 fn spawn_git_worker(cx: &mut Context<GitStore>) -> mpsc::UnboundedSender<GitJob> {
1433 let (job_tx, mut job_rx) = mpsc::unbounded::<GitJob>();
1434
1435 cx.spawn(async move |_, cx| {
1436 let mut jobs = VecDeque::new();
1437 loop {
1438 while let Ok(Some(next_job)) = job_rx.try_next() {
1439 jobs.push_back(next_job);
1440 }
1441
1442 if let Some(job) = jobs.pop_front() {
1443 if let Some(current_key) = &job.key {
1444 if jobs
1445 .iter()
1446 .any(|other_job| other_job.key.as_ref() == Some(current_key))
1447 {
1448 continue;
1449 }
1450 }
1451 (job.job)(cx).await;
1452 } else if let Some(job) = job_rx.next().await {
1453 jobs.push_back(job);
1454 } else {
1455 break;
1456 }
1457 }
1458 })
1459 .detach();
1460 job_tx
1461 }
1462
1463 pub fn git_init(
1464 &self,
1465 path: Arc<Path>,
1466 fallback_branch_name: String,
1467 cx: &App,
1468 ) -> Task<Result<()>> {
1469 match &self.state {
1470 GitStoreState::Local { fs, .. } => {
1471 let fs = fs.clone();
1472 cx.background_executor()
1473 .spawn(async move { fs.git_init(&path, fallback_branch_name) })
1474 }
1475 GitStoreState::Ssh {
1476 upstream_client,
1477 upstream_project_id: project_id,
1478 ..
1479 }
1480 | GitStoreState::Remote {
1481 upstream_client,
1482 project_id,
1483 ..
1484 } => {
1485 let client = upstream_client.clone();
1486 let project_id = *project_id;
1487 cx.background_executor().spawn(async move {
1488 client
1489 .request(proto::GitInit {
1490 project_id: project_id.0,
1491 abs_path: path.to_string_lossy().to_string(),
1492 fallback_branch_name,
1493 })
1494 .await?;
1495 Ok(())
1496 })
1497 }
1498 }
1499 }
1500
1501 async fn handle_update_repository(
1502 this: Entity<Self>,
1503 envelope: TypedEnvelope<proto::UpdateRepository>,
1504 mut cx: AsyncApp,
1505 ) -> Result<()> {
1506 this.update(&mut cx, |this, cx| {
1507 let mut update = envelope.payload;
1508
1509 let work_directory_id = ProjectEntryId::from_proto(update.id);
1510 let client = this
1511 .upstream_client()
1512 .context("no upstream client")?
1513 .clone();
1514
1515 let repo = this
1516 .repositories
1517 .entry(work_directory_id)
1518 .or_insert_with(|| {
1519 let git_store = cx.weak_entity();
1520
1521 cx.new(|_| Repository {
1522 commit_message_buffer: None,
1523 git_store,
1524 project_environment: None,
1525 worktree_id: None,
1526 repository_entry: RepositoryEntry {
1527 work_directory_id,
1528 current_branch: None,
1529 statuses_by_path: Default::default(),
1530 current_merge_conflicts: Default::default(),
1531 work_directory_abs_path: update.abs_path.clone().into(),
1532 worktree_scan_id: update.scan_id as usize,
1533 },
1534 merge_message: None,
1535 completed_scan_id: update.scan_id as usize,
1536 state: RepositoryState::Remote {
1537 project_id: ProjectId(update.project_id),
1538 client,
1539 work_directory_id,
1540 },
1541 job_sender: this.update_sender.clone(),
1542 askpass_delegates: Default::default(),
1543 latest_askpass_id: 0,
1544 })
1545 });
1546
1547 repo.update(cx, |repo, _cx| repo.apply_remote_update(update.clone()))?;
1548 cx.emit(GitEvent::GitStateUpdated);
1549 this.active_repo_id.get_or_insert_with(|| {
1550 cx.emit(GitEvent::ActiveRepositoryChanged);
1551 work_directory_id
1552 });
1553
1554 if let Some((client, project_id)) = this.downstream_client() {
1555 update.project_id = project_id.to_proto();
1556 client.send(update).log_err();
1557 }
1558 Ok(())
1559 })?
1560 }
1561
1562 async fn handle_remove_repository(
1563 this: Entity<Self>,
1564 envelope: TypedEnvelope<proto::RemoveRepository>,
1565 mut cx: AsyncApp,
1566 ) -> Result<()> {
1567 this.update(&mut cx, |this, cx| {
1568 let mut update = envelope.payload;
1569 let id = ProjectEntryId::from_proto(update.id);
1570 this.repositories.remove(&id);
1571 if let Some((client, project_id)) = this.downstream_client() {
1572 update.project_id = project_id.to_proto();
1573 client.send(update).log_err();
1574 }
1575 if this.active_repo_id == Some(id) {
1576 this.active_repo_id = None;
1577 cx.emit(GitEvent::ActiveRepositoryChanged);
1578 }
1579 cx.emit(GitEvent::GitStateUpdated);
1580 })
1581 }
1582
1583 async fn handle_git_init(
1584 this: Entity<Self>,
1585 envelope: TypedEnvelope<proto::GitInit>,
1586 cx: AsyncApp,
1587 ) -> Result<proto::Ack> {
1588 let path: Arc<Path> = PathBuf::from(envelope.payload.abs_path).into();
1589 let name = envelope.payload.fallback_branch_name;
1590 cx.update(|cx| this.read(cx).git_init(path, name, cx))?
1591 .await?;
1592
1593 Ok(proto::Ack {})
1594 }
1595
1596 async fn handle_fetch(
1597 this: Entity<Self>,
1598 envelope: TypedEnvelope<proto::Fetch>,
1599 mut cx: AsyncApp,
1600 ) -> Result<proto::RemoteMessageResponse> {
1601 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1602 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1603 let askpass_id = envelope.payload.askpass_id;
1604
1605 let askpass = make_remote_delegate(
1606 this,
1607 envelope.payload.project_id,
1608 work_directory_id,
1609 askpass_id,
1610 &mut cx,
1611 );
1612
1613 let remote_output = repository_handle
1614 .update(&mut cx, |repository_handle, cx| {
1615 repository_handle.fetch(askpass, cx)
1616 })?
1617 .await??;
1618
1619 Ok(proto::RemoteMessageResponse {
1620 stdout: remote_output.stdout,
1621 stderr: remote_output.stderr,
1622 })
1623 }
1624
1625 async fn handle_push(
1626 this: Entity<Self>,
1627 envelope: TypedEnvelope<proto::Push>,
1628 mut cx: AsyncApp,
1629 ) -> Result<proto::RemoteMessageResponse> {
1630 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1631 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1632
1633 let askpass_id = envelope.payload.askpass_id;
1634 let askpass = make_remote_delegate(
1635 this,
1636 envelope.payload.project_id,
1637 work_directory_id,
1638 askpass_id,
1639 &mut cx,
1640 );
1641
1642 let options = envelope
1643 .payload
1644 .options
1645 .as_ref()
1646 .map(|_| match envelope.payload.options() {
1647 proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream,
1648 proto::push::PushOptions::Force => git::repository::PushOptions::Force,
1649 });
1650
1651 let branch_name = envelope.payload.branch_name.into();
1652 let remote_name = envelope.payload.remote_name.into();
1653
1654 let remote_output = repository_handle
1655 .update(&mut cx, |repository_handle, cx| {
1656 repository_handle.push(branch_name, remote_name, options, askpass, cx)
1657 })?
1658 .await??;
1659 Ok(proto::RemoteMessageResponse {
1660 stdout: remote_output.stdout,
1661 stderr: remote_output.stderr,
1662 })
1663 }
1664
1665 async fn handle_pull(
1666 this: Entity<Self>,
1667 envelope: TypedEnvelope<proto::Pull>,
1668 mut cx: AsyncApp,
1669 ) -> Result<proto::RemoteMessageResponse> {
1670 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1671 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1672 let askpass_id = envelope.payload.askpass_id;
1673 let askpass = make_remote_delegate(
1674 this,
1675 envelope.payload.project_id,
1676 work_directory_id,
1677 askpass_id,
1678 &mut cx,
1679 );
1680
1681 let branch_name = envelope.payload.branch_name.into();
1682 let remote_name = envelope.payload.remote_name.into();
1683
1684 let remote_message = repository_handle
1685 .update(&mut cx, |repository_handle, cx| {
1686 repository_handle.pull(branch_name, remote_name, askpass, cx)
1687 })?
1688 .await??;
1689
1690 Ok(proto::RemoteMessageResponse {
1691 stdout: remote_message.stdout,
1692 stderr: remote_message.stderr,
1693 })
1694 }
1695
1696 async fn handle_stage(
1697 this: Entity<Self>,
1698 envelope: TypedEnvelope<proto::Stage>,
1699 mut cx: AsyncApp,
1700 ) -> Result<proto::Ack> {
1701 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1702 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1703
1704 let entries = envelope
1705 .payload
1706 .paths
1707 .into_iter()
1708 .map(PathBuf::from)
1709 .map(RepoPath::new)
1710 .collect();
1711
1712 repository_handle
1713 .update(&mut cx, |repository_handle, cx| {
1714 repository_handle.stage_entries(entries, cx)
1715 })?
1716 .await?;
1717 Ok(proto::Ack {})
1718 }
1719
1720 async fn handle_unstage(
1721 this: Entity<Self>,
1722 envelope: TypedEnvelope<proto::Unstage>,
1723 mut cx: AsyncApp,
1724 ) -> Result<proto::Ack> {
1725 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1726 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1727
1728 let entries = envelope
1729 .payload
1730 .paths
1731 .into_iter()
1732 .map(PathBuf::from)
1733 .map(RepoPath::new)
1734 .collect();
1735
1736 repository_handle
1737 .update(&mut cx, |repository_handle, cx| {
1738 repository_handle.unstage_entries(entries, cx)
1739 })?
1740 .await?;
1741
1742 Ok(proto::Ack {})
1743 }
1744
1745 async fn handle_set_index_text(
1746 this: Entity<Self>,
1747 envelope: TypedEnvelope<proto::SetIndexText>,
1748 mut cx: AsyncApp,
1749 ) -> Result<proto::Ack> {
1750 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1751 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1752
1753 repository_handle
1754 .update(&mut cx, |repository_handle, cx| {
1755 repository_handle.spawn_set_index_text_job(
1756 RepoPath::from_str(&envelope.payload.path),
1757 envelope.payload.text,
1758 cx,
1759 )
1760 })?
1761 .await??;
1762 Ok(proto::Ack {})
1763 }
1764
1765 async fn handle_commit(
1766 this: Entity<Self>,
1767 envelope: TypedEnvelope<proto::Commit>,
1768 mut cx: AsyncApp,
1769 ) -> Result<proto::Ack> {
1770 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1771 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1772
1773 let message = SharedString::from(envelope.payload.message);
1774 let name = envelope.payload.name.map(SharedString::from);
1775 let email = envelope.payload.email.map(SharedString::from);
1776
1777 repository_handle
1778 .update(&mut cx, |repository_handle, cx| {
1779 repository_handle.commit(message, name.zip(email), cx)
1780 })?
1781 .await??;
1782 Ok(proto::Ack {})
1783 }
1784
1785 async fn handle_get_remotes(
1786 this: Entity<Self>,
1787 envelope: TypedEnvelope<proto::GetRemotes>,
1788 mut cx: AsyncApp,
1789 ) -> Result<proto::GetRemotesResponse> {
1790 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1791 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1792
1793 let branch_name = envelope.payload.branch_name;
1794
1795 let remotes = repository_handle
1796 .update(&mut cx, |repository_handle, _| {
1797 repository_handle.get_remotes(branch_name)
1798 })?
1799 .await??;
1800
1801 Ok(proto::GetRemotesResponse {
1802 remotes: remotes
1803 .into_iter()
1804 .map(|remotes| proto::get_remotes_response::Remote {
1805 name: remotes.name.to_string(),
1806 })
1807 .collect::<Vec<_>>(),
1808 })
1809 }
1810
1811 async fn handle_get_branches(
1812 this: Entity<Self>,
1813 envelope: TypedEnvelope<proto::GitGetBranches>,
1814 mut cx: AsyncApp,
1815 ) -> Result<proto::GitBranchesResponse> {
1816 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1817 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1818
1819 let branches = repository_handle
1820 .update(&mut cx, |repository_handle, _| repository_handle.branches())?
1821 .await??;
1822
1823 Ok(proto::GitBranchesResponse {
1824 branches: branches
1825 .into_iter()
1826 .map(|branch| worktree::branch_to_proto(&branch))
1827 .collect::<Vec<_>>(),
1828 })
1829 }
1830 async fn handle_create_branch(
1831 this: Entity<Self>,
1832 envelope: TypedEnvelope<proto::GitCreateBranch>,
1833 mut cx: AsyncApp,
1834 ) -> Result<proto::Ack> {
1835 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1836 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1837 let branch_name = envelope.payload.branch_name;
1838
1839 repository_handle
1840 .update(&mut cx, |repository_handle, _| {
1841 repository_handle.create_branch(branch_name)
1842 })?
1843 .await??;
1844
1845 Ok(proto::Ack {})
1846 }
1847
1848 async fn handle_change_branch(
1849 this: Entity<Self>,
1850 envelope: TypedEnvelope<proto::GitChangeBranch>,
1851 mut cx: AsyncApp,
1852 ) -> Result<proto::Ack> {
1853 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1854 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1855 let branch_name = envelope.payload.branch_name;
1856
1857 repository_handle
1858 .update(&mut cx, |repository_handle, _| {
1859 repository_handle.change_branch(branch_name)
1860 })?
1861 .await??;
1862
1863 Ok(proto::Ack {})
1864 }
1865
1866 async fn handle_show(
1867 this: Entity<Self>,
1868 envelope: TypedEnvelope<proto::GitShow>,
1869 mut cx: AsyncApp,
1870 ) -> Result<proto::GitCommitDetails> {
1871 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1872 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1873
1874 let commit = repository_handle
1875 .update(&mut cx, |repository_handle, _| {
1876 repository_handle.show(envelope.payload.commit)
1877 })?
1878 .await??;
1879 Ok(proto::GitCommitDetails {
1880 sha: commit.sha.into(),
1881 message: commit.message.into(),
1882 commit_timestamp: commit.commit_timestamp,
1883 committer_email: commit.committer_email.into(),
1884 committer_name: commit.committer_name.into(),
1885 })
1886 }
1887
1888 async fn handle_reset(
1889 this: Entity<Self>,
1890 envelope: TypedEnvelope<proto::GitReset>,
1891 mut cx: AsyncApp,
1892 ) -> Result<proto::Ack> {
1893 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1894 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1895
1896 let mode = match envelope.payload.mode() {
1897 git_reset::ResetMode::Soft => ResetMode::Soft,
1898 git_reset::ResetMode::Mixed => ResetMode::Mixed,
1899 };
1900
1901 repository_handle
1902 .update(&mut cx, |repository_handle, cx| {
1903 repository_handle.reset(envelope.payload.commit, mode, cx)
1904 })?
1905 .await??;
1906 Ok(proto::Ack {})
1907 }
1908
1909 async fn handle_checkout_files(
1910 this: Entity<Self>,
1911 envelope: TypedEnvelope<proto::GitCheckoutFiles>,
1912 mut cx: AsyncApp,
1913 ) -> Result<proto::Ack> {
1914 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1915 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1916 let paths = envelope
1917 .payload
1918 .paths
1919 .iter()
1920 .map(|s| RepoPath::from_str(s))
1921 .collect();
1922
1923 repository_handle
1924 .update(&mut cx, |repository_handle, cx| {
1925 repository_handle.checkout_files(&envelope.payload.commit, paths, cx)
1926 })?
1927 .await??;
1928 Ok(proto::Ack {})
1929 }
1930
1931 async fn handle_open_commit_message_buffer(
1932 this: Entity<Self>,
1933 envelope: TypedEnvelope<proto::OpenCommitMessageBuffer>,
1934 mut cx: AsyncApp,
1935 ) -> Result<proto::OpenBufferResponse> {
1936 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1937 let repository = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1938 let buffer = repository
1939 .update(&mut cx, |repository, cx| {
1940 repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx)
1941 })?
1942 .await?;
1943
1944 let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?;
1945 this.update(&mut cx, |this, cx| {
1946 this.buffer_store.update(cx, |buffer_store, cx| {
1947 buffer_store
1948 .create_buffer_for_peer(
1949 &buffer,
1950 envelope.original_sender_id.unwrap_or(envelope.sender_id),
1951 cx,
1952 )
1953 .detach_and_log_err(cx);
1954 })
1955 })?;
1956
1957 Ok(proto::OpenBufferResponse {
1958 buffer_id: buffer_id.to_proto(),
1959 })
1960 }
1961
1962 async fn handle_askpass(
1963 this: Entity<Self>,
1964 envelope: TypedEnvelope<proto::AskPassRequest>,
1965 mut cx: AsyncApp,
1966 ) -> Result<proto::AskPassResponse> {
1967 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1968 let repository = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1969
1970 let delegates = cx.update(|cx| repository.read(cx).askpass_delegates.clone())?;
1971 let Some(mut askpass) = delegates.lock().remove(&envelope.payload.askpass_id) else {
1972 debug_panic!("no askpass found");
1973 return Err(anyhow::anyhow!("no askpass found"));
1974 };
1975
1976 let response = askpass.ask_password(envelope.payload.prompt).await?;
1977
1978 delegates
1979 .lock()
1980 .insert(envelope.payload.askpass_id, askpass);
1981
1982 Ok(proto::AskPassResponse { response })
1983 }
1984
1985 async fn handle_check_for_pushed_commits(
1986 this: Entity<Self>,
1987 envelope: TypedEnvelope<proto::CheckForPushedCommits>,
1988 mut cx: AsyncApp,
1989 ) -> Result<proto::CheckForPushedCommitsResponse> {
1990 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
1991 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
1992
1993 let branches = repository_handle
1994 .update(&mut cx, |repository_handle, _| {
1995 repository_handle.check_for_pushed_commits()
1996 })?
1997 .await??;
1998 Ok(proto::CheckForPushedCommitsResponse {
1999 pushed_to: branches
2000 .into_iter()
2001 .map(|commit| commit.to_string())
2002 .collect(),
2003 })
2004 }
2005
2006 async fn handle_git_diff(
2007 this: Entity<Self>,
2008 envelope: TypedEnvelope<proto::GitDiff>,
2009 mut cx: AsyncApp,
2010 ) -> Result<proto::GitDiffResponse> {
2011 let work_directory_id = ProjectEntryId::from_proto(envelope.payload.work_directory_id);
2012 let repository_handle = Self::repository_for_request(&this, work_directory_id, &mut cx)?;
2013 let diff_type = match envelope.payload.diff_type() {
2014 proto::git_diff::DiffType::HeadToIndex => DiffType::HeadToIndex,
2015 proto::git_diff::DiffType::HeadToWorktree => DiffType::HeadToWorktree,
2016 };
2017
2018 let mut diff = repository_handle
2019 .update(&mut cx, |repository_handle, cx| {
2020 repository_handle.diff(diff_type, cx)
2021 })?
2022 .await??;
2023 const ONE_MB: usize = 1_000_000;
2024 if diff.len() > ONE_MB {
2025 diff = diff.chars().take(ONE_MB).collect()
2026 }
2027
2028 Ok(proto::GitDiffResponse { diff })
2029 }
2030
2031 async fn handle_open_unstaged_diff(
2032 this: Entity<Self>,
2033 request: TypedEnvelope<proto::OpenUnstagedDiff>,
2034 mut cx: AsyncApp,
2035 ) -> Result<proto::OpenUnstagedDiffResponse> {
2036 let buffer_id = BufferId::new(request.payload.buffer_id)?;
2037 let diff = this
2038 .update(&mut cx, |this, cx| {
2039 let buffer = this.buffer_store.read(cx).get(buffer_id)?;
2040 Some(this.open_unstaged_diff(buffer, cx))
2041 })?
2042 .ok_or_else(|| anyhow!("no such buffer"))?
2043 .await?;
2044 this.update(&mut cx, |this, _| {
2045 let shared_diffs = this
2046 .shared_diffs
2047 .entry(request.original_sender_id.unwrap_or(request.sender_id))
2048 .or_default();
2049 shared_diffs.entry(buffer_id).or_default().unstaged = Some(diff.clone());
2050 })?;
2051 let staged_text = diff.read_with(&cx, |diff, _| diff.base_text_string())?;
2052 Ok(proto::OpenUnstagedDiffResponse { staged_text })
2053 }
2054
2055 async fn handle_open_uncommitted_diff(
2056 this: Entity<Self>,
2057 request: TypedEnvelope<proto::OpenUncommittedDiff>,
2058 mut cx: AsyncApp,
2059 ) -> Result<proto::OpenUncommittedDiffResponse> {
2060 let buffer_id = BufferId::new(request.payload.buffer_id)?;
2061 let diff = this
2062 .update(&mut cx, |this, cx| {
2063 let buffer = this.buffer_store.read(cx).get(buffer_id)?;
2064 Some(this.open_uncommitted_diff(buffer, cx))
2065 })?
2066 .ok_or_else(|| anyhow!("no such buffer"))?
2067 .await?;
2068 this.update(&mut cx, |this, _| {
2069 let shared_diffs = this
2070 .shared_diffs
2071 .entry(request.original_sender_id.unwrap_or(request.sender_id))
2072 .or_default();
2073 shared_diffs.entry(buffer_id).or_default().uncommitted = Some(diff.clone());
2074 })?;
2075 diff.read_with(&cx, |diff, cx| {
2076 use proto::open_uncommitted_diff_response::Mode;
2077
2078 let unstaged_diff = diff.secondary_diff();
2079 let index_snapshot = unstaged_diff.and_then(|diff| {
2080 let diff = diff.read(cx);
2081 diff.base_text_exists().then(|| diff.base_text())
2082 });
2083
2084 let mode;
2085 let staged_text;
2086 let committed_text;
2087 if diff.base_text_exists() {
2088 let committed_snapshot = diff.base_text();
2089 committed_text = Some(committed_snapshot.text());
2090 if let Some(index_text) = index_snapshot {
2091 if index_text.remote_id() == committed_snapshot.remote_id() {
2092 mode = Mode::IndexMatchesHead;
2093 staged_text = None;
2094 } else {
2095 mode = Mode::IndexAndHead;
2096 staged_text = Some(index_text.text());
2097 }
2098 } else {
2099 mode = Mode::IndexAndHead;
2100 staged_text = None;
2101 }
2102 } else {
2103 mode = Mode::IndexAndHead;
2104 committed_text = None;
2105 staged_text = index_snapshot.as_ref().map(|buffer| buffer.text());
2106 }
2107
2108 proto::OpenUncommittedDiffResponse {
2109 committed_text,
2110 staged_text,
2111 mode: mode.into(),
2112 }
2113 })
2114 }
2115
2116 async fn handle_update_diff_bases(
2117 this: Entity<Self>,
2118 request: TypedEnvelope<proto::UpdateDiffBases>,
2119 mut cx: AsyncApp,
2120 ) -> Result<()> {
2121 let buffer_id = BufferId::new(request.payload.buffer_id)?;
2122 this.update(&mut cx, |this, cx| {
2123 if let Some(diff_state) = this.diffs.get_mut(&buffer_id) {
2124 if let Some(buffer) = this.buffer_store.read(cx).get(buffer_id) {
2125 let buffer = buffer.read(cx).text_snapshot();
2126 diff_state.update(cx, |diff_state, cx| {
2127 diff_state.handle_base_texts_updated(buffer, request.payload, cx);
2128 })
2129 }
2130 }
2131 })
2132 }
2133
2134 async fn handle_blame_buffer(
2135 this: Entity<Self>,
2136 envelope: TypedEnvelope<proto::BlameBuffer>,
2137 mut cx: AsyncApp,
2138 ) -> Result<proto::BlameBufferResponse> {
2139 let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
2140 let version = deserialize_version(&envelope.payload.version);
2141 let buffer = this.read_with(&cx, |this, cx| {
2142 this.buffer_store.read(cx).get_existing(buffer_id)
2143 })??;
2144 buffer
2145 .update(&mut cx, |buffer, _| {
2146 buffer.wait_for_version(version.clone())
2147 })?
2148 .await?;
2149 let blame = this
2150 .update(&mut cx, |this, cx| {
2151 this.blame_buffer(&buffer, Some(version), cx)
2152 })?
2153 .await?;
2154 Ok(serialize_blame_buffer_response(blame))
2155 }
2156
2157 async fn handle_get_permalink_to_line(
2158 this: Entity<Self>,
2159 envelope: TypedEnvelope<proto::GetPermalinkToLine>,
2160 mut cx: AsyncApp,
2161 ) -> Result<proto::GetPermalinkToLineResponse> {
2162 let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
2163 // let version = deserialize_version(&envelope.payload.version);
2164 let selection = {
2165 let proto_selection = envelope
2166 .payload
2167 .selection
2168 .context("no selection to get permalink for defined")?;
2169 proto_selection.start as u32..proto_selection.end as u32
2170 };
2171 let buffer = this.read_with(&cx, |this, cx| {
2172 this.buffer_store.read(cx).get_existing(buffer_id)
2173 })??;
2174 let permalink = this
2175 .update(&mut cx, |this, cx| {
2176 this.get_permalink_to_line(&buffer, selection, cx)
2177 })?
2178 .await?;
2179 Ok(proto::GetPermalinkToLineResponse {
2180 permalink: permalink.to_string(),
2181 })
2182 }
2183
2184 fn repository_for_request(
2185 this: &Entity<Self>,
2186 work_directory_id: ProjectEntryId,
2187 cx: &mut AsyncApp,
2188 ) -> Result<Entity<Repository>> {
2189 this.update(cx, |this, cx| {
2190 this.repositories
2191 .values()
2192 .find(|repository_handle| {
2193 repository_handle
2194 .read(cx)
2195 .repository_entry
2196 .work_directory_id()
2197 == work_directory_id
2198 })
2199 .context("missing repository handle")
2200 .cloned()
2201 })?
2202 }
2203
2204 pub fn repo_snapshots(&self, cx: &App) -> HashMap<ProjectEntryId, RepositoryEntry> {
2205 self.repositories
2206 .iter()
2207 .map(|(id, repo)| (*id, repo.read(cx).repository_entry.clone()))
2208 .collect()
2209 }
2210}
2211
2212impl BufferDiffState {
2213 fn buffer_language_changed(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
2214 self.language = buffer.read(cx).language().cloned();
2215 self.language_changed = true;
2216 let _ = self.recalculate_diffs(
2217 buffer.read(cx).text_snapshot(),
2218 self.hunk_staging_operation_count,
2219 cx,
2220 );
2221 }
2222
2223 fn unstaged_diff(&self) -> Option<Entity<BufferDiff>> {
2224 self.unstaged_diff.as_ref().and_then(|set| set.upgrade())
2225 }
2226
2227 fn uncommitted_diff(&self) -> Option<Entity<BufferDiff>> {
2228 self.uncommitted_diff.as_ref().and_then(|set| set.upgrade())
2229 }
2230
2231 fn handle_base_texts_updated(
2232 &mut self,
2233 buffer: text::BufferSnapshot,
2234 message: proto::UpdateDiffBases,
2235 cx: &mut Context<Self>,
2236 ) {
2237 use proto::update_diff_bases::Mode;
2238
2239 let Some(mode) = Mode::from_i32(message.mode) else {
2240 return;
2241 };
2242
2243 let diff_bases_change = match mode {
2244 Mode::HeadOnly => DiffBasesChange::SetHead(message.committed_text),
2245 Mode::IndexOnly => DiffBasesChange::SetIndex(message.staged_text),
2246 Mode::IndexMatchesHead => DiffBasesChange::SetBoth(message.committed_text),
2247 Mode::IndexAndHead => DiffBasesChange::SetEach {
2248 index: message.staged_text,
2249 head: message.committed_text,
2250 },
2251 };
2252
2253 let _ = self.diff_bases_changed(
2254 buffer,
2255 diff_bases_change,
2256 self.hunk_staging_operation_count,
2257 cx,
2258 );
2259 }
2260
2261 pub fn wait_for_recalculation(&mut self) -> Option<oneshot::Receiver<()>> {
2262 if self.diff_updated_futures.is_empty() {
2263 return None;
2264 }
2265 let (tx, rx) = oneshot::channel();
2266 self.diff_updated_futures.push(tx);
2267 Some(rx)
2268 }
2269
2270 fn diff_bases_changed(
2271 &mut self,
2272 buffer: text::BufferSnapshot,
2273 diff_bases_change: DiffBasesChange,
2274 prev_hunk_staging_operation_count: usize,
2275 cx: &mut Context<Self>,
2276 ) -> oneshot::Receiver<()> {
2277 match diff_bases_change {
2278 DiffBasesChange::SetIndex(index) => {
2279 self.index_text = index.map(|mut index| {
2280 text::LineEnding::normalize(&mut index);
2281 Arc::new(index)
2282 });
2283 self.index_changed = true;
2284 }
2285 DiffBasesChange::SetHead(head) => {
2286 self.head_text = head.map(|mut head| {
2287 text::LineEnding::normalize(&mut head);
2288 Arc::new(head)
2289 });
2290 self.head_changed = true;
2291 }
2292 DiffBasesChange::SetBoth(text) => {
2293 let text = text.map(|mut text| {
2294 text::LineEnding::normalize(&mut text);
2295 Arc::new(text)
2296 });
2297 self.head_text = text.clone();
2298 self.index_text = text;
2299 self.head_changed = true;
2300 self.index_changed = true;
2301 }
2302 DiffBasesChange::SetEach { index, head } => {
2303 self.index_text = index.map(|mut index| {
2304 text::LineEnding::normalize(&mut index);
2305 Arc::new(index)
2306 });
2307 self.index_changed = true;
2308 self.head_text = head.map(|mut head| {
2309 text::LineEnding::normalize(&mut head);
2310 Arc::new(head)
2311 });
2312 self.head_changed = true;
2313 }
2314 }
2315
2316 self.recalculate_diffs(buffer, prev_hunk_staging_operation_count, cx)
2317 }
2318
2319 fn recalculate_diffs(
2320 &mut self,
2321 buffer: text::BufferSnapshot,
2322 prev_hunk_staging_operation_count: usize,
2323 cx: &mut Context<Self>,
2324 ) -> oneshot::Receiver<()> {
2325 log::debug!("recalculate diffs");
2326 let (tx, rx) = oneshot::channel();
2327 self.diff_updated_futures.push(tx);
2328
2329 let language = self.language.clone();
2330 let language_registry = self.language_registry.clone();
2331 let unstaged_diff = self.unstaged_diff();
2332 let uncommitted_diff = self.uncommitted_diff();
2333 let head = self.head_text.clone();
2334 let index = self.index_text.clone();
2335 let index_changed = self.index_changed;
2336 let head_changed = self.head_changed;
2337 let language_changed = self.language_changed;
2338 let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) {
2339 (Some(index), Some(head)) => Arc::ptr_eq(index, head),
2340 (None, None) => true,
2341 _ => false,
2342 };
2343 self.recalculate_diff_task = Some(cx.spawn(async move |this, cx| {
2344 let mut new_unstaged_diff = None;
2345 if let Some(unstaged_diff) = &unstaged_diff {
2346 new_unstaged_diff = Some(
2347 BufferDiff::update_diff(
2348 unstaged_diff.clone(),
2349 buffer.clone(),
2350 index,
2351 index_changed,
2352 language_changed,
2353 language.clone(),
2354 language_registry.clone(),
2355 cx,
2356 )
2357 .await?,
2358 );
2359 }
2360
2361 let mut new_uncommitted_diff = None;
2362 if let Some(uncommitted_diff) = &uncommitted_diff {
2363 new_uncommitted_diff = if index_matches_head {
2364 new_unstaged_diff.clone()
2365 } else {
2366 Some(
2367 BufferDiff::update_diff(
2368 uncommitted_diff.clone(),
2369 buffer.clone(),
2370 head,
2371 head_changed,
2372 language_changed,
2373 language.clone(),
2374 language_registry.clone(),
2375 cx,
2376 )
2377 .await?,
2378 )
2379 }
2380 }
2381
2382 if this.update(cx, |this, _| {
2383 this.hunk_staging_operation_count > prev_hunk_staging_operation_count
2384 })? {
2385 return Ok(());
2386 }
2387
2388 let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) =
2389 unstaged_diff.as_ref().zip(new_unstaged_diff.clone())
2390 {
2391 unstaged_diff.update(cx, |diff, cx| {
2392 diff.set_snapshot(&buffer, new_unstaged_diff, language_changed, None, cx)
2393 })?
2394 } else {
2395 None
2396 };
2397
2398 if let Some((uncommitted_diff, new_uncommitted_diff)) =
2399 uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone())
2400 {
2401 uncommitted_diff.update(cx, |uncommitted_diff, cx| {
2402 uncommitted_diff.set_snapshot(
2403 &buffer,
2404 new_uncommitted_diff,
2405 language_changed,
2406 unstaged_changed_range,
2407 cx,
2408 );
2409 })?;
2410 }
2411
2412 if let Some(this) = this.upgrade() {
2413 this.update(cx, |this, _| {
2414 this.index_changed = false;
2415 this.head_changed = false;
2416 this.language_changed = false;
2417 for tx in this.diff_updated_futures.drain(..) {
2418 tx.send(()).ok();
2419 }
2420 })?;
2421 }
2422
2423 Ok(())
2424 }));
2425
2426 rx
2427 }
2428}
2429
2430fn make_remote_delegate(
2431 this: Entity<GitStore>,
2432 project_id: u64,
2433 work_directory_id: ProjectEntryId,
2434 askpass_id: u64,
2435 cx: &mut AsyncApp,
2436) -> AskPassDelegate {
2437 AskPassDelegate::new(cx, move |prompt, tx, cx| {
2438 this.update(cx, |this, cx| {
2439 let Some((client, _)) = this.downstream_client() else {
2440 return;
2441 };
2442 let response = client.request(proto::AskPassRequest {
2443 project_id,
2444 work_directory_id: work_directory_id.to_proto(),
2445 askpass_id,
2446 prompt,
2447 });
2448 cx.spawn(async move |_, _| {
2449 tx.send(response.await?.response).ok();
2450 anyhow::Ok(())
2451 })
2452 .detach_and_log_err(cx);
2453 })
2454 .log_err();
2455 })
2456}
2457
2458impl GitStoreState {
2459 fn load_staged_text(
2460 &self,
2461 buffer: &Entity<Buffer>,
2462 buffer_store: &Entity<BufferStore>,
2463 cx: &App,
2464 ) -> Task<Result<Option<String>>> {
2465 match self {
2466 GitStoreState::Local { .. } => {
2467 if let Some((worktree, path)) =
2468 buffer_store.read(cx).worktree_for_buffer(buffer, cx)
2469 {
2470 worktree.read(cx).load_staged_file(path.as_ref(), cx)
2471 } else {
2472 return Task::ready(Err(anyhow!("no such worktree")));
2473 }
2474 }
2475 GitStoreState::Ssh {
2476 upstream_client,
2477 upstream_project_id: project_id,
2478 ..
2479 }
2480 | GitStoreState::Remote {
2481 upstream_client,
2482 project_id,
2483 } => {
2484 let buffer_id = buffer.read(cx).remote_id();
2485 let project_id = *project_id;
2486 let client = upstream_client.clone();
2487 cx.background_spawn(async move {
2488 let response = client
2489 .request(proto::OpenUnstagedDiff {
2490 project_id: project_id.to_proto(),
2491 buffer_id: buffer_id.to_proto(),
2492 })
2493 .await?;
2494 Ok(response.staged_text)
2495 })
2496 }
2497 }
2498 }
2499
2500 fn load_committed_text(
2501 &self,
2502 buffer: &Entity<Buffer>,
2503 buffer_store: &Entity<BufferStore>,
2504 cx: &App,
2505 ) -> Task<Result<DiffBasesChange>> {
2506 match self {
2507 GitStoreState::Local { .. } => {
2508 if let Some((worktree, path)) =
2509 buffer_store.read(cx).worktree_for_buffer(buffer, cx)
2510 {
2511 let worktree = worktree.read(cx);
2512 let committed_text = worktree.load_committed_file(&path, cx);
2513 let staged_text = worktree.load_staged_file(&path, cx);
2514 cx.background_spawn(async move {
2515 let committed_text = committed_text.await?;
2516 let staged_text = staged_text.await?;
2517 let diff_bases_change = if committed_text == staged_text {
2518 DiffBasesChange::SetBoth(committed_text)
2519 } else {
2520 DiffBasesChange::SetEach {
2521 index: staged_text,
2522 head: committed_text,
2523 }
2524 };
2525 Ok(diff_bases_change)
2526 })
2527 } else {
2528 Task::ready(Err(anyhow!("no such worktree")))
2529 }
2530 }
2531 GitStoreState::Ssh {
2532 upstream_client,
2533 upstream_project_id: project_id,
2534 ..
2535 }
2536 | GitStoreState::Remote {
2537 upstream_client,
2538 project_id,
2539 } => {
2540 use proto::open_uncommitted_diff_response::Mode;
2541
2542 let buffer_id = buffer.read(cx).remote_id();
2543 let project_id = *project_id;
2544 let client = upstream_client.clone();
2545 cx.background_spawn(async move {
2546 let response = client
2547 .request(proto::OpenUncommittedDiff {
2548 project_id: project_id.to_proto(),
2549 buffer_id: buffer_id.to_proto(),
2550 })
2551 .await?;
2552 let mode =
2553 Mode::from_i32(response.mode).ok_or_else(|| anyhow!("Invalid mode"))?;
2554 let bases = match mode {
2555 Mode::IndexMatchesHead => DiffBasesChange::SetBoth(response.committed_text),
2556 Mode::IndexAndHead => DiffBasesChange::SetEach {
2557 head: response.committed_text,
2558 index: response.staged_text,
2559 },
2560 };
2561 Ok(bases)
2562 })
2563 }
2564 }
2565 }
2566}
2567
2568impl Repository {
2569 pub fn git_store(&self) -> Option<Entity<GitStore>> {
2570 self.git_store.upgrade()
2571 }
2572
2573 fn id(&self) -> ProjectEntryId {
2574 self.repository_entry.work_directory_id()
2575 }
2576
2577 pub fn current_branch(&self) -> Option<&Branch> {
2578 self.repository_entry.branch()
2579 }
2580
2581 pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
2582 self.repository_entry.status_for_path(path)
2583 }
2584
2585 fn send_job<F, Fut, R>(&self, job: F) -> oneshot::Receiver<R>
2586 where
2587 F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static,
2588 Fut: Future<Output = R> + 'static,
2589 R: Send + 'static,
2590 {
2591 self.send_keyed_job(None, job)
2592 }
2593
2594 fn send_keyed_job<F, Fut, R>(&self, key: Option<GitJobKey>, job: F) -> oneshot::Receiver<R>
2595 where
2596 F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static,
2597 Fut: Future<Output = R> + 'static,
2598 R: Send + 'static,
2599 {
2600 let (result_tx, result_rx) = futures::channel::oneshot::channel();
2601 let git_repo = self.state.clone();
2602 self.job_sender
2603 .unbounded_send(GitJob {
2604 key,
2605 job: Box::new(|cx: &mut AsyncApp| {
2606 let job = job(git_repo, cx.clone());
2607 cx.spawn(async move |_| {
2608 let result = job.await;
2609 result_tx.send(result).ok();
2610 })
2611 }),
2612 })
2613 .ok();
2614 result_rx
2615 }
2616
2617 /// This is the name that will be displayed in the repository selector for this repository.
2618 pub fn display_name(&self) -> SharedString {
2619 self.repository_entry
2620 .work_directory_abs_path
2621 .file_name()
2622 .unwrap_or_default()
2623 .to_string_lossy()
2624 .to_string()
2625 .into()
2626 }
2627
2628 pub fn set_as_active_repository(&self, cx: &mut Context<Self>) {
2629 let Some(git_store) = self.git_store.upgrade() else {
2630 return;
2631 };
2632 let entity = cx.entity();
2633 git_store.update(cx, |git_store, cx| {
2634 let Some((&id, _)) = git_store
2635 .repositories
2636 .iter()
2637 .find(|(_, handle)| *handle == &entity)
2638 else {
2639 return;
2640 };
2641 git_store.active_repo_id = Some(id);
2642 cx.emit(GitEvent::ActiveRepositoryChanged);
2643 });
2644 }
2645
2646 pub fn cached_status(&self) -> impl '_ + Iterator<Item = StatusEntry> {
2647 self.repository_entry.status()
2648 }
2649
2650 pub fn load_index_text(&self, path: RepoPath) -> oneshot::Receiver<Option<String>> {
2651 self.send_job(move |repo, _cx| async move {
2652 match repo {
2653 RepositoryState::Local(git_repository) => {
2654 git_repository.load_index_text(path).await
2655 }
2656 RepositoryState::Remote { .. } => None,
2657 }
2658 })
2659 }
2660
2661 pub fn has_conflict(&self, path: &RepoPath) -> bool {
2662 self.repository_entry
2663 .current_merge_conflicts
2664 .contains(&path)
2665 }
2666
2667 pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option<ProjectPath> {
2668 let git_store = self.git_store.upgrade()?;
2669 let worktree_store = git_store.read(cx).worktree_store.read(cx);
2670 let abs_path = self.repository_entry.work_directory_abs_path.join(&path.0);
2671 let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?;
2672 Some(ProjectPath {
2673 worktree_id: worktree.read(cx).id(),
2674 path: relative_path.into(),
2675 })
2676 }
2677
2678 pub fn project_path_to_repo_path(&self, path: &ProjectPath, cx: &App) -> Option<RepoPath> {
2679 let git_store = self.git_store.upgrade()?;
2680 let worktree_store = git_store.read(cx).worktree_store.read(cx);
2681 let abs_path = worktree_store.absolutize(path, cx)?;
2682 self.repository_entry.relativize_abs_path(&abs_path)
2683 }
2684
2685 pub fn contains_sub_repo(&self, other: &Entity<Self>, cx: &App) -> bool {
2686 other
2687 .read(cx)
2688 .repository_entry
2689 .work_directory_abs_path
2690 .starts_with(&self.repository_entry.work_directory_abs_path)
2691 }
2692
2693 pub fn local_repository(&self) -> Option<Arc<dyn GitRepository>> {
2694 match &self.state {
2695 RepositoryState::Local(git_repository) => Some(git_repository.clone()),
2696 RepositoryState::Remote { .. } => None,
2697 }
2698 }
2699
2700 pub fn open_commit_buffer(
2701 &mut self,
2702 languages: Option<Arc<LanguageRegistry>>,
2703 buffer_store: Entity<BufferStore>,
2704 cx: &mut Context<Self>,
2705 ) -> Task<Result<Entity<Buffer>>> {
2706 if let Some(buffer) = self.commit_message_buffer.clone() {
2707 return Task::ready(Ok(buffer));
2708 }
2709
2710 if let RepositoryState::Remote {
2711 project_id,
2712 client,
2713 work_directory_id,
2714 } = self.state.clone()
2715 {
2716 let client = client.clone();
2717 cx.spawn(async move |repository, cx| {
2718 let request = client.request(proto::OpenCommitMessageBuffer {
2719 project_id: project_id.0,
2720 work_directory_id: work_directory_id.to_proto(),
2721 });
2722 let response = request.await.context("requesting to open commit buffer")?;
2723 let buffer_id = BufferId::new(response.buffer_id)?;
2724 let buffer = buffer_store
2725 .update(cx, |buffer_store, cx| {
2726 buffer_store.wait_for_remote_buffer(buffer_id, cx)
2727 })?
2728 .await?;
2729 if let Some(language_registry) = languages {
2730 let git_commit_language =
2731 language_registry.language_for_name("Git Commit").await?;
2732 buffer.update(cx, |buffer, cx| {
2733 buffer.set_language(Some(git_commit_language), cx);
2734 })?;
2735 }
2736 repository.update(cx, |repository, _| {
2737 repository.commit_message_buffer = Some(buffer.clone());
2738 })?;
2739 Ok(buffer)
2740 })
2741 } else {
2742 self.open_local_commit_buffer(languages, buffer_store, cx)
2743 }
2744 }
2745
2746 fn open_local_commit_buffer(
2747 &mut self,
2748 language_registry: Option<Arc<LanguageRegistry>>,
2749 buffer_store: Entity<BufferStore>,
2750 cx: &mut Context<Self>,
2751 ) -> Task<Result<Entity<Buffer>>> {
2752 cx.spawn(async move |repository, cx| {
2753 let buffer = buffer_store
2754 .update(cx, |buffer_store, cx| buffer_store.create_buffer(cx))?
2755 .await?;
2756
2757 if let Some(language_registry) = language_registry {
2758 let git_commit_language = language_registry.language_for_name("Git Commit").await?;
2759 buffer.update(cx, |buffer, cx| {
2760 buffer.set_language(Some(git_commit_language), cx);
2761 })?;
2762 }
2763
2764 repository.update(cx, |repository, _| {
2765 repository.commit_message_buffer = Some(buffer.clone());
2766 })?;
2767 Ok(buffer)
2768 })
2769 }
2770
2771 pub fn checkout_files(
2772 &self,
2773 commit: &str,
2774 paths: Vec<RepoPath>,
2775 cx: &mut App,
2776 ) -> oneshot::Receiver<Result<()>> {
2777 let commit = commit.to_string();
2778 let env = self.worktree_environment(cx);
2779
2780 self.send_job(|git_repo, _| async move {
2781 match git_repo {
2782 RepositoryState::Local(repo) => repo.checkout_files(commit, paths, env.await).await,
2783 RepositoryState::Remote {
2784 project_id,
2785 client,
2786 work_directory_id,
2787 } => {
2788 client
2789 .request(proto::GitCheckoutFiles {
2790 project_id: project_id.0,
2791 work_directory_id: work_directory_id.to_proto(),
2792 commit,
2793 paths: paths
2794 .into_iter()
2795 .map(|p| p.to_string_lossy().to_string())
2796 .collect(),
2797 })
2798 .await?;
2799
2800 Ok(())
2801 }
2802 }
2803 })
2804 }
2805
2806 pub fn reset(
2807 &self,
2808 commit: String,
2809 reset_mode: ResetMode,
2810 cx: &mut App,
2811 ) -> oneshot::Receiver<Result<()>> {
2812 let commit = commit.to_string();
2813 let env = self.worktree_environment(cx);
2814 self.send_job(|git_repo, _| async move {
2815 match git_repo {
2816 RepositoryState::Local(git_repo) => {
2817 let env = env.await;
2818 git_repo.reset(commit, reset_mode, env).await
2819 }
2820 RepositoryState::Remote {
2821 project_id,
2822 client,
2823 work_directory_id,
2824 } => {
2825 client
2826 .request(proto::GitReset {
2827 project_id: project_id.0,
2828 work_directory_id: work_directory_id.to_proto(),
2829 commit,
2830 mode: match reset_mode {
2831 ResetMode::Soft => git_reset::ResetMode::Soft.into(),
2832 ResetMode::Mixed => git_reset::ResetMode::Mixed.into(),
2833 },
2834 })
2835 .await?;
2836
2837 Ok(())
2838 }
2839 }
2840 })
2841 }
2842
2843 pub fn show(&self, commit: String) -> oneshot::Receiver<Result<CommitDetails>> {
2844 self.send_job(|git_repo, _cx| async move {
2845 match git_repo {
2846 RepositoryState::Local(git_repository) => git_repository.show(commit).await,
2847 RepositoryState::Remote {
2848 project_id,
2849 client,
2850 work_directory_id,
2851 } => {
2852 let resp = client
2853 .request(proto::GitShow {
2854 project_id: project_id.0,
2855 work_directory_id: work_directory_id.to_proto(),
2856 commit,
2857 })
2858 .await?;
2859
2860 Ok(CommitDetails {
2861 sha: resp.sha.into(),
2862 message: resp.message.into(),
2863 commit_timestamp: resp.commit_timestamp,
2864 committer_email: resp.committer_email.into(),
2865 committer_name: resp.committer_name.into(),
2866 })
2867 }
2868 }
2869 })
2870 }
2871
2872 fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
2873 Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
2874 }
2875
2876 pub fn stage_entries(
2877 &self,
2878 entries: Vec<RepoPath>,
2879 cx: &mut Context<Self>,
2880 ) -> Task<anyhow::Result<()>> {
2881 if entries.is_empty() {
2882 return Task::ready(Ok(()));
2883 }
2884 let env = self.worktree_environment(cx);
2885
2886 let mut save_futures = Vec::new();
2887 if let Some(buffer_store) = self.buffer_store(cx) {
2888 buffer_store.update(cx, |buffer_store, cx| {
2889 for path in &entries {
2890 let Some(project_path) = self.repo_path_to_project_path(path, cx) else {
2891 continue;
2892 };
2893 if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
2894 if buffer
2895 .read(cx)
2896 .file()
2897 .map_or(false, |file| file.disk_state().exists())
2898 {
2899 save_futures.push(buffer_store.save_buffer(buffer, cx));
2900 }
2901 }
2902 }
2903 })
2904 }
2905
2906 cx.spawn(async move |this, cx| {
2907 for save_future in save_futures {
2908 save_future.await?;
2909 }
2910 let env = env.await;
2911
2912 this.update(cx, |this, _| {
2913 this.send_job(|git_repo, _cx| async move {
2914 match git_repo {
2915 RepositoryState::Local(repo) => repo.stage_paths(entries, env).await,
2916 RepositoryState::Remote {
2917 project_id,
2918 client,
2919 work_directory_id,
2920 } => {
2921 client
2922 .request(proto::Stage {
2923 project_id: project_id.0,
2924 work_directory_id: work_directory_id.to_proto(),
2925 paths: entries
2926 .into_iter()
2927 .map(|repo_path| repo_path.as_ref().to_proto())
2928 .collect(),
2929 })
2930 .await
2931 .context("sending stage request")?;
2932
2933 Ok(())
2934 }
2935 }
2936 })
2937 })?
2938 .await??;
2939
2940 Ok(())
2941 })
2942 }
2943
2944 pub fn unstage_entries(
2945 &self,
2946 entries: Vec<RepoPath>,
2947 cx: &mut Context<Self>,
2948 ) -> Task<anyhow::Result<()>> {
2949 if entries.is_empty() {
2950 return Task::ready(Ok(()));
2951 }
2952 let env = self.worktree_environment(cx);
2953
2954 let mut save_futures = Vec::new();
2955 if let Some(buffer_store) = self.buffer_store(cx) {
2956 buffer_store.update(cx, |buffer_store, cx| {
2957 for path in &entries {
2958 let Some(project_path) = self.repo_path_to_project_path(path, cx) else {
2959 continue;
2960 };
2961 if let Some(buffer) = buffer_store.get_by_path(&project_path, cx) {
2962 if buffer
2963 .read(cx)
2964 .file()
2965 .map_or(false, |file| file.disk_state().exists())
2966 {
2967 save_futures.push(buffer_store.save_buffer(buffer, cx));
2968 }
2969 }
2970 }
2971 })
2972 }
2973
2974 cx.spawn(async move |this, cx| {
2975 for save_future in save_futures {
2976 save_future.await?;
2977 }
2978 let env = env.await;
2979
2980 this.update(cx, |this, _| {
2981 this.send_job(|git_repo, _cx| async move {
2982 match git_repo {
2983 RepositoryState::Local(repo) => repo.unstage_paths(entries, env).await,
2984 RepositoryState::Remote {
2985 project_id,
2986 client,
2987 work_directory_id,
2988 } => {
2989 client
2990 .request(proto::Unstage {
2991 project_id: project_id.0,
2992 work_directory_id: work_directory_id.to_proto(),
2993 paths: entries
2994 .into_iter()
2995 .map(|repo_path| repo_path.as_ref().to_proto())
2996 .collect(),
2997 })
2998 .await
2999 .context("sending unstage request")?;
3000
3001 Ok(())
3002 }
3003 }
3004 })
3005 })?
3006 .await??;
3007
3008 Ok(())
3009 })
3010 }
3011
3012 pub fn stage_all(&self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
3013 let to_stage = self
3014 .repository_entry
3015 .status()
3016 .filter(|entry| !entry.status.staging().is_fully_staged())
3017 .map(|entry| entry.repo_path.clone())
3018 .collect();
3019 self.stage_entries(to_stage, cx)
3020 }
3021
3022 pub fn unstage_all(&self, cx: &mut Context<Self>) -> Task<anyhow::Result<()>> {
3023 let to_unstage = self
3024 .repository_entry
3025 .status()
3026 .filter(|entry| entry.status.staging().has_staged())
3027 .map(|entry| entry.repo_path.clone())
3028 .collect();
3029 self.unstage_entries(to_unstage, cx)
3030 }
3031
3032 /// Get a count of all entries in the active repository, including
3033 /// untracked files.
3034 pub fn entry_count(&self) -> usize {
3035 self.repository_entry.status_len()
3036 }
3037
3038 fn worktree_environment(
3039 &self,
3040 cx: &mut App,
3041 ) -> impl Future<Output = HashMap<String, String>> + 'static {
3042 let task = self.project_environment.as_ref().and_then(|env| {
3043 env.update(cx, |env, cx| {
3044 env.get_environment(
3045 self.worktree_id,
3046 Some(
3047 self.repository_entry
3048 .work_directory_abs_path
3049 .as_path()
3050 .into(),
3051 ),
3052 cx,
3053 )
3054 })
3055 .ok()
3056 });
3057 async move { OptionFuture::from(task).await.flatten().unwrap_or_default() }
3058 }
3059
3060 pub fn commit(
3061 &self,
3062 message: SharedString,
3063 name_and_email: Option<(SharedString, SharedString)>,
3064 cx: &mut App,
3065 ) -> oneshot::Receiver<Result<()>> {
3066 let env = self.worktree_environment(cx);
3067 self.send_job(|git_repo, _cx| async move {
3068 match git_repo {
3069 RepositoryState::Local(repo) => {
3070 let env = env.await;
3071 repo.commit(message, name_and_email, env).await
3072 }
3073 RepositoryState::Remote {
3074 project_id,
3075 client,
3076 work_directory_id,
3077 } => {
3078 let (name, email) = name_and_email.unzip();
3079 client
3080 .request(proto::Commit {
3081 project_id: project_id.0,
3082 work_directory_id: work_directory_id.to_proto(),
3083 message: String::from(message),
3084 name: name.map(String::from),
3085 email: email.map(String::from),
3086 })
3087 .await
3088 .context("sending commit request")?;
3089
3090 Ok(())
3091 }
3092 }
3093 })
3094 }
3095
3096 pub fn fetch(
3097 &mut self,
3098 askpass: AskPassDelegate,
3099 cx: &mut App,
3100 ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
3101 let askpass_delegates = self.askpass_delegates.clone();
3102 let askpass_id = util::post_inc(&mut self.latest_askpass_id);
3103 let env = self.worktree_environment(cx);
3104
3105 self.send_job(move |git_repo, cx| async move {
3106 match git_repo {
3107 RepositoryState::Local(git_repository) => {
3108 let env = env.await;
3109 git_repository.fetch(askpass, env, cx).await
3110 }
3111 RepositoryState::Remote {
3112 project_id,
3113 client,
3114 work_directory_id,
3115 } => {
3116 askpass_delegates.lock().insert(askpass_id, askpass);
3117 let _defer = util::defer(|| {
3118 let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
3119 debug_assert!(askpass_delegate.is_some());
3120 });
3121
3122 let response = client
3123 .request(proto::Fetch {
3124 project_id: project_id.0,
3125 work_directory_id: work_directory_id.to_proto(),
3126 askpass_id,
3127 })
3128 .await
3129 .context("sending fetch request")?;
3130
3131 Ok(RemoteCommandOutput {
3132 stdout: response.stdout,
3133 stderr: response.stderr,
3134 })
3135 }
3136 }
3137 })
3138 }
3139
3140 pub fn push(
3141 &mut self,
3142 branch: SharedString,
3143 remote: SharedString,
3144 options: Option<PushOptions>,
3145 askpass: AskPassDelegate,
3146 cx: &mut App,
3147 ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
3148 let askpass_delegates = self.askpass_delegates.clone();
3149 let askpass_id = util::post_inc(&mut self.latest_askpass_id);
3150 let env = self.worktree_environment(cx);
3151
3152 self.send_job(move |git_repo, cx| async move {
3153 match git_repo {
3154 RepositoryState::Local(git_repository) => {
3155 let env = env.await;
3156 // let askpass = AskPassSession::new(&executor, askpass).await?;
3157 git_repository
3158 .push(
3159 branch.to_string(),
3160 remote.to_string(),
3161 options,
3162 askpass,
3163 env,
3164 cx,
3165 )
3166 .await
3167 }
3168 RepositoryState::Remote {
3169 project_id,
3170 client,
3171 work_directory_id,
3172 } => {
3173 askpass_delegates.lock().insert(askpass_id, askpass);
3174 let _defer = util::defer(|| {
3175 let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
3176 debug_assert!(askpass_delegate.is_some());
3177 });
3178 let response = client
3179 .request(proto::Push {
3180 project_id: project_id.0,
3181 work_directory_id: work_directory_id.to_proto(),
3182 askpass_id,
3183 branch_name: branch.to_string(),
3184 remote_name: remote.to_string(),
3185 options: options.map(|options| match options {
3186 PushOptions::Force => proto::push::PushOptions::Force,
3187 PushOptions::SetUpstream => proto::push::PushOptions::SetUpstream,
3188 } as i32),
3189 })
3190 .await
3191 .context("sending push request")?;
3192
3193 Ok(RemoteCommandOutput {
3194 stdout: response.stdout,
3195 stderr: response.stderr,
3196 })
3197 }
3198 }
3199 })
3200 }
3201
3202 pub fn pull(
3203 &mut self,
3204 branch: SharedString,
3205 remote: SharedString,
3206 askpass: AskPassDelegate,
3207 cx: &mut App,
3208 ) -> oneshot::Receiver<Result<RemoteCommandOutput>> {
3209 let askpass_delegates = self.askpass_delegates.clone();
3210 let askpass_id = util::post_inc(&mut self.latest_askpass_id);
3211 let env = self.worktree_environment(cx);
3212
3213 self.send_job(move |git_repo, cx| async move {
3214 match git_repo {
3215 RepositoryState::Local(git_repository) => {
3216 let env = env.await;
3217 git_repository
3218 .pull(branch.to_string(), remote.to_string(), askpass, env, cx)
3219 .await
3220 }
3221 RepositoryState::Remote {
3222 project_id,
3223 client,
3224 work_directory_id,
3225 } => {
3226 askpass_delegates.lock().insert(askpass_id, askpass);
3227 let _defer = util::defer(|| {
3228 let askpass_delegate = askpass_delegates.lock().remove(&askpass_id);
3229 debug_assert!(askpass_delegate.is_some());
3230 });
3231 let response = client
3232 .request(proto::Pull {
3233 project_id: project_id.0,
3234 work_directory_id: work_directory_id.to_proto(),
3235 askpass_id,
3236 branch_name: branch.to_string(),
3237 remote_name: remote.to_string(),
3238 })
3239 .await
3240 .context("sending pull request")?;
3241
3242 Ok(RemoteCommandOutput {
3243 stdout: response.stdout,
3244 stderr: response.stderr,
3245 })
3246 }
3247 }
3248 })
3249 }
3250
3251 fn spawn_set_index_text_job(
3252 &self,
3253 path: RepoPath,
3254 content: Option<String>,
3255 cx: &mut App,
3256 ) -> oneshot::Receiver<anyhow::Result<()>> {
3257 let env = self.worktree_environment(cx);
3258
3259 self.send_keyed_job(
3260 Some(GitJobKey::WriteIndex(path.clone())),
3261 |git_repo, _cx| async {
3262 match git_repo {
3263 RepositoryState::Local(repo) => {
3264 repo.set_index_text(path, content, env.await).await
3265 }
3266 RepositoryState::Remote {
3267 project_id,
3268 client,
3269 work_directory_id,
3270 } => {
3271 client
3272 .request(proto::SetIndexText {
3273 project_id: project_id.0,
3274 work_directory_id: work_directory_id.to_proto(),
3275 path: path.as_ref().to_proto(),
3276 text: content,
3277 })
3278 .await?;
3279 Ok(())
3280 }
3281 }
3282 },
3283 )
3284 }
3285
3286 pub fn get_remotes(
3287 &self,
3288 branch_name: Option<String>,
3289 ) -> oneshot::Receiver<Result<Vec<Remote>>> {
3290 self.send_job(|repo, _cx| async move {
3291 match repo {
3292 RepositoryState::Local(git_repository) => {
3293 git_repository.get_remotes(branch_name).await
3294 }
3295 RepositoryState::Remote {
3296 project_id,
3297 client,
3298 work_directory_id,
3299 } => {
3300 let response = client
3301 .request(proto::GetRemotes {
3302 project_id: project_id.0,
3303 work_directory_id: work_directory_id.to_proto(),
3304 branch_name,
3305 })
3306 .await?;
3307
3308 let remotes = response
3309 .remotes
3310 .into_iter()
3311 .map(|remotes| git::repository::Remote {
3312 name: remotes.name.into(),
3313 })
3314 .collect();
3315
3316 Ok(remotes)
3317 }
3318 }
3319 })
3320 }
3321
3322 pub fn branch(&self) -> Option<&Branch> {
3323 self.repository_entry.branch()
3324 }
3325
3326 pub fn branches(&self) -> oneshot::Receiver<Result<Vec<Branch>>> {
3327 self.send_job(|repo, cx| async move {
3328 match repo {
3329 RepositoryState::Local(git_repository) => {
3330 let git_repository = git_repository.clone();
3331 cx.background_spawn(async move { git_repository.branches().await })
3332 .await
3333 }
3334 RepositoryState::Remote {
3335 project_id,
3336 client,
3337 work_directory_id,
3338 } => {
3339 let response = client
3340 .request(proto::GitGetBranches {
3341 project_id: project_id.0,
3342 work_directory_id: work_directory_id.to_proto(),
3343 })
3344 .await?;
3345
3346 let branches = response
3347 .branches
3348 .into_iter()
3349 .map(|branch| worktree::proto_to_branch(&branch))
3350 .collect();
3351
3352 Ok(branches)
3353 }
3354 }
3355 })
3356 }
3357
3358 pub fn diff(&self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver<Result<String>> {
3359 self.send_job(|repo, _cx| async move {
3360 match repo {
3361 RepositoryState::Local(git_repository) => git_repository.diff(diff_type).await,
3362 RepositoryState::Remote {
3363 project_id,
3364 client,
3365 work_directory_id,
3366 ..
3367 } => {
3368 let response = client
3369 .request(proto::GitDiff {
3370 project_id: project_id.0,
3371 work_directory_id: work_directory_id.to_proto(),
3372 diff_type: match diff_type {
3373 DiffType::HeadToIndex => {
3374 proto::git_diff::DiffType::HeadToIndex.into()
3375 }
3376 DiffType::HeadToWorktree => {
3377 proto::git_diff::DiffType::HeadToWorktree.into()
3378 }
3379 },
3380 })
3381 .await?;
3382
3383 Ok(response.diff)
3384 }
3385 }
3386 })
3387 }
3388
3389 pub fn create_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
3390 self.send_job(|repo, _cx| async move {
3391 match repo {
3392 RepositoryState::Local(git_repository) => {
3393 git_repository.create_branch(branch_name).await
3394 }
3395 RepositoryState::Remote {
3396 project_id,
3397 client,
3398 work_directory_id,
3399 } => {
3400 client
3401 .request(proto::GitCreateBranch {
3402 project_id: project_id.0,
3403 work_directory_id: work_directory_id.to_proto(),
3404 branch_name,
3405 })
3406 .await?;
3407
3408 Ok(())
3409 }
3410 }
3411 })
3412 }
3413
3414 pub fn change_branch(&self, branch_name: String) -> oneshot::Receiver<Result<()>> {
3415 self.send_job(|repo, _cx| async move {
3416 match repo {
3417 RepositoryState::Local(git_repository) => {
3418 git_repository.change_branch(branch_name).await
3419 }
3420 RepositoryState::Remote {
3421 project_id,
3422 client,
3423 work_directory_id,
3424 } => {
3425 client
3426 .request(proto::GitChangeBranch {
3427 project_id: project_id.0,
3428 work_directory_id: work_directory_id.to_proto(),
3429 branch_name,
3430 })
3431 .await?;
3432
3433 Ok(())
3434 }
3435 }
3436 })
3437 }
3438
3439 pub fn check_for_pushed_commits(&self) -> oneshot::Receiver<Result<Vec<SharedString>>> {
3440 self.send_job(|repo, _cx| async move {
3441 match repo {
3442 RepositoryState::Local(git_repository) => {
3443 git_repository.check_for_pushed_commit().await
3444 }
3445 RepositoryState::Remote {
3446 project_id,
3447 client,
3448 work_directory_id,
3449 } => {
3450 let response = client
3451 .request(proto::CheckForPushedCommits {
3452 project_id: project_id.0,
3453 work_directory_id: work_directory_id.to_proto(),
3454 })
3455 .await?;
3456
3457 let branches = response.pushed_to.into_iter().map(Into::into).collect();
3458
3459 Ok(branches)
3460 }
3461 }
3462 })
3463 }
3464
3465 pub fn checkpoint(&self) -> oneshot::Receiver<Result<GitRepositoryCheckpoint>> {
3466 self.send_job(|repo, _cx| async move {
3467 match repo {
3468 RepositoryState::Local(git_repository) => git_repository.checkpoint().await,
3469 RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
3470 }
3471 })
3472 }
3473
3474 pub fn restore_checkpoint(
3475 &self,
3476 checkpoint: GitRepositoryCheckpoint,
3477 ) -> oneshot::Receiver<Result<()>> {
3478 self.send_job(move |repo, _cx| async move {
3479 match repo {
3480 RepositoryState::Local(git_repository) => {
3481 git_repository.restore_checkpoint(checkpoint).await
3482 }
3483 RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
3484 }
3485 })
3486 }
3487
3488 pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateRepository) -> Result<()> {
3489 let conflicted_paths = TreeSet::from_ordered_entries(
3490 update
3491 .current_merge_conflicts
3492 .into_iter()
3493 .map(|path| RepoPath(Path::new(&path).into())),
3494 );
3495 self.repository_entry.current_branch = update.branch_summary.as_ref().map(proto_to_branch);
3496 self.repository_entry.current_merge_conflicts = conflicted_paths;
3497
3498 let edits = update
3499 .removed_statuses
3500 .into_iter()
3501 .map(|path| sum_tree::Edit::Remove(PathKey(FromProto::from_proto(path))))
3502 .chain(
3503 update
3504 .updated_statuses
3505 .into_iter()
3506 .filter_map(|updated_status| {
3507 Some(sum_tree::Edit::Insert(updated_status.try_into().log_err()?))
3508 }),
3509 )
3510 .collect::<Vec<_>>();
3511 self.repository_entry.statuses_by_path.edit(edits, &());
3512 Ok(())
3513 }
3514
3515 pub fn compare_checkpoints(
3516 &self,
3517 left: GitRepositoryCheckpoint,
3518 right: GitRepositoryCheckpoint,
3519 ) -> oneshot::Receiver<Result<bool>> {
3520 self.send_job(move |repo, _cx| async move {
3521 match repo {
3522 RepositoryState::Local(git_repository) => {
3523 git_repository.compare_checkpoints(left, right).await
3524 }
3525 RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
3526 }
3527 })
3528 }
3529
3530 pub fn delete_checkpoint(
3531 &self,
3532 checkpoint: GitRepositoryCheckpoint,
3533 ) -> oneshot::Receiver<Result<()>> {
3534 self.send_job(move |repo, _cx| async move {
3535 match repo {
3536 RepositoryState::Local(git_repository) => {
3537 git_repository.delete_checkpoint(checkpoint).await
3538 }
3539 RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
3540 }
3541 })
3542 }
3543
3544 pub fn diff_checkpoints(
3545 &self,
3546 base_checkpoint: GitRepositoryCheckpoint,
3547 target_checkpoint: GitRepositoryCheckpoint,
3548 ) -> oneshot::Receiver<Result<String>> {
3549 self.send_job(move |repo, _cx| async move {
3550 match repo {
3551 RepositoryState::Local(git_repository) => {
3552 git_repository
3553 .diff_checkpoints(base_checkpoint, target_checkpoint)
3554 .await
3555 }
3556 RepositoryState::Remote { .. } => Err(anyhow!("not implemented yet")),
3557 }
3558 })
3559 }
3560}
3561
3562fn get_permalink_in_rust_registry_src(
3563 provider_registry: Arc<GitHostingProviderRegistry>,
3564 path: PathBuf,
3565 selection: Range<u32>,
3566) -> Result<url::Url> {
3567 #[derive(Deserialize)]
3568 struct CargoVcsGit {
3569 sha1: String,
3570 }
3571
3572 #[derive(Deserialize)]
3573 struct CargoVcsInfo {
3574 git: CargoVcsGit,
3575 path_in_vcs: String,
3576 }
3577
3578 #[derive(Deserialize)]
3579 struct CargoPackage {
3580 repository: String,
3581 }
3582
3583 #[derive(Deserialize)]
3584 struct CargoToml {
3585 package: CargoPackage,
3586 }
3587
3588 let Some((dir, cargo_vcs_info_json)) = path.ancestors().skip(1).find_map(|dir| {
3589 let json = std::fs::read_to_string(dir.join(".cargo_vcs_info.json")).ok()?;
3590 Some((dir, json))
3591 }) else {
3592 bail!("No .cargo_vcs_info.json found in parent directories")
3593 };
3594 let cargo_vcs_info = serde_json::from_str::<CargoVcsInfo>(&cargo_vcs_info_json)?;
3595 let cargo_toml = std::fs::read_to_string(dir.join("Cargo.toml"))?;
3596 let manifest = toml::from_str::<CargoToml>(&cargo_toml)?;
3597 let (provider, remote) = parse_git_remote_url(provider_registry, &manifest.package.repository)
3598 .ok_or_else(|| anyhow!("Failed to parse package.repository field of manifest"))?;
3599 let path = PathBuf::from(cargo_vcs_info.path_in_vcs).join(path.strip_prefix(dir).unwrap());
3600 let permalink = provider.build_permalink(
3601 remote,
3602 BuildPermalinkParams {
3603 sha: &cargo_vcs_info.git.sha1,
3604 path: &path.to_string_lossy(),
3605 selection: Some(selection),
3606 },
3607 );
3608 Ok(permalink)
3609}
3610
3611fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> proto::BlameBufferResponse {
3612 let Some(blame) = blame else {
3613 return proto::BlameBufferResponse {
3614 blame_response: None,
3615 };
3616 };
3617
3618 let entries = blame
3619 .entries
3620 .into_iter()
3621 .map(|entry| proto::BlameEntry {
3622 sha: entry.sha.as_bytes().into(),
3623 start_line: entry.range.start,
3624 end_line: entry.range.end,
3625 original_line_number: entry.original_line_number,
3626 author: entry.author.clone(),
3627 author_mail: entry.author_mail.clone(),
3628 author_time: entry.author_time,
3629 author_tz: entry.author_tz.clone(),
3630 committer: entry.committer_name.clone(),
3631 committer_mail: entry.committer_email.clone(),
3632 committer_time: entry.committer_time,
3633 committer_tz: entry.committer_tz.clone(),
3634 summary: entry.summary.clone(),
3635 previous: entry.previous.clone(),
3636 filename: entry.filename.clone(),
3637 })
3638 .collect::<Vec<_>>();
3639
3640 let messages = blame
3641 .messages
3642 .into_iter()
3643 .map(|(oid, message)| proto::CommitMessage {
3644 oid: oid.as_bytes().into(),
3645 message,
3646 })
3647 .collect::<Vec<_>>();
3648
3649 proto::BlameBufferResponse {
3650 blame_response: Some(proto::blame_buffer_response::BlameResponse {
3651 entries,
3652 messages,
3653 remote_url: blame.remote_url,
3654 }),
3655 }
3656}
3657
3658fn deserialize_blame_buffer_response(
3659 response: proto::BlameBufferResponse,
3660) -> Option<git::blame::Blame> {
3661 let response = response.blame_response?;
3662 let entries = response
3663 .entries
3664 .into_iter()
3665 .filter_map(|entry| {
3666 Some(git::blame::BlameEntry {
3667 sha: git::Oid::from_bytes(&entry.sha).ok()?,
3668 range: entry.start_line..entry.end_line,
3669 original_line_number: entry.original_line_number,
3670 committer_name: entry.committer,
3671 committer_time: entry.committer_time,
3672 committer_tz: entry.committer_tz,
3673 committer_email: entry.committer_mail,
3674 author: entry.author,
3675 author_mail: entry.author_mail,
3676 author_time: entry.author_time,
3677 author_tz: entry.author_tz,
3678 summary: entry.summary,
3679 previous: entry.previous,
3680 filename: entry.filename,
3681 })
3682 })
3683 .collect::<Vec<_>>();
3684
3685 let messages = response
3686 .messages
3687 .into_iter()
3688 .filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message)))
3689 .collect::<HashMap<_, _>>();
3690
3691 Some(Blame {
3692 entries,
3693 messages,
3694 remote_url: response.remote_url,
3695 })
3696}