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