1use crate::status::FileStatus;
2use crate::GitHostingProviderRegistry;
3use crate::{blame::Blame, status::GitStatus};
4use anyhow::{anyhow, Context, Result};
5use collections::{HashMap, HashSet};
6use git2::BranchType;
7use gpui::SharedString;
8use parking_lot::Mutex;
9use rope::Rope;
10use schemars::JsonSchema;
11use serde::Deserialize;
12use std::borrow::Borrow;
13use std::io::Write as _;
14use std::process::Stdio;
15use std::sync::LazyLock;
16use std::{
17 cmp::Ordering,
18 path::{Component, Path, PathBuf},
19 sync::Arc,
20};
21use sum_tree::MapSeekTarget;
22use util::command::new_std_command;
23use util::ResultExt;
24
25#[derive(Clone, Debug, Hash, PartialEq, Eq)]
26pub struct Branch {
27 pub is_head: bool,
28 pub name: SharedString,
29 pub upstream: Option<Upstream>,
30 pub most_recent_commit: Option<CommitSummary>,
31}
32
33impl Branch {
34 pub fn tracking_status(&self) -> Option<UpstreamTrackingStatus> {
35 self.upstream
36 .as_ref()
37 .and_then(|upstream| upstream.tracking.status())
38 }
39
40 pub fn priority_key(&self) -> (bool, Option<i64>) {
41 (
42 self.is_head,
43 self.most_recent_commit
44 .as_ref()
45 .map(|commit| commit.commit_timestamp),
46 )
47 }
48}
49
50#[derive(Clone, Debug, Hash, PartialEq, Eq)]
51pub struct Upstream {
52 pub ref_name: SharedString,
53 pub tracking: UpstreamTracking,
54}
55
56#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
57pub enum UpstreamTracking {
58 /// Remote ref not present in local repository.
59 Gone,
60 /// Remote ref present in local repository (fetched from remote).
61 Tracked(UpstreamTrackingStatus),
62}
63
64impl UpstreamTracking {
65 pub fn is_gone(&self) -> bool {
66 matches!(self, UpstreamTracking::Gone)
67 }
68
69 pub fn status(&self) -> Option<UpstreamTrackingStatus> {
70 match self {
71 UpstreamTracking::Gone => None,
72 UpstreamTracking::Tracked(status) => Some(*status),
73 }
74 }
75}
76
77#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
78pub struct UpstreamTrackingStatus {
79 pub ahead: u32,
80 pub behind: u32,
81}
82
83#[derive(Clone, Debug, Hash, PartialEq, Eq)]
84pub struct CommitSummary {
85 pub sha: SharedString,
86 pub subject: SharedString,
87 /// This is a unix timestamp
88 pub commit_timestamp: i64,
89}
90
91#[derive(Clone, Debug, Hash, PartialEq, Eq)]
92pub struct CommitDetails {
93 pub sha: SharedString,
94 pub message: SharedString,
95 pub commit_timestamp: i64,
96 pub committer_email: SharedString,
97 pub committer_name: SharedString,
98}
99
100#[derive(Debug, Clone, Hash, PartialEq, Eq)]
101pub struct Remote {
102 pub name: SharedString,
103}
104
105pub enum ResetMode {
106 // reset the branch pointer, leave index and worktree unchanged
107 // (this will make it look like things that were committed are now
108 // staged)
109 Soft,
110 // reset the branch pointer and index, leave worktree unchanged
111 // (this makes it look as though things that were committed are now
112 // unstaged)
113 Mixed,
114}
115
116pub trait GitRepository: Send + Sync {
117 fn reload_index(&self);
118
119 /// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
120 ///
121 /// Also returns `None` for symlinks.
122 fn load_index_text(&self, path: &RepoPath) -> Option<String>;
123
124 /// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path.
125 ///
126 /// Also returns `None` for symlinks.
127 fn load_committed_text(&self, path: &RepoPath) -> Option<String>;
128
129 fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()>;
130
131 /// Returns the URL of the remote with the given name.
132 fn remote_url(&self, name: &str) -> Option<String>;
133
134 /// Returns the SHA of the current HEAD.
135 fn head_sha(&self) -> Option<String>;
136
137 fn merge_head_shas(&self) -> Vec<String>;
138
139 /// Returns the list of git statuses, sorted by path
140 fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
141
142 fn branches(&self) -> Result<Vec<Branch>>;
143 fn change_branch(&self, _: &str) -> Result<()>;
144 fn create_branch(&self, _: &str) -> Result<()>;
145 fn branch_exits(&self, _: &str) -> Result<bool>;
146
147 fn reset(&self, commit: &str, mode: ResetMode) -> Result<()>;
148 fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()>;
149
150 fn show(&self, commit: &str) -> Result<CommitDetails>;
151
152 fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
153
154 /// Returns the absolute path to the repository. For worktrees, this will be the path to the
155 /// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
156 fn path(&self) -> PathBuf;
157
158 /// Returns the absolute path to the ".git" dir for the main repository, typically a `.git`
159 /// folder. For worktrees, this will be the path to the repository the worktree was created
160 /// from. Otherwise, this is the same value as `path()`.
161 ///
162 /// Git documentation calls this the "commondir", and for git CLI is overridden by
163 /// `GIT_COMMON_DIR`.
164 fn main_repository_path(&self) -> PathBuf;
165
166 /// Updates the index to match the worktree at the given paths.
167 ///
168 /// If any of the paths have been deleted from the worktree, they will be removed from the index if found there.
169 fn stage_paths(&self, paths: &[RepoPath]) -> Result<()>;
170 /// Updates the index to match HEAD at the given paths.
171 ///
172 /// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
173 fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
174
175 fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
176
177 fn push(
178 &self,
179 branch_name: &str,
180 upstream_name: &str,
181 options: Option<PushOptions>,
182 ) -> Result<()>;
183 fn pull(&self, branch_name: &str, upstream_name: &str) -> Result<()>;
184 fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>>;
185 fn fetch(&self) -> Result<()>;
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
189pub enum PushOptions {
190 SetUpstream,
191 Force,
192}
193
194impl std::fmt::Debug for dyn GitRepository {
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 f.debug_struct("dyn GitRepository<...>").finish()
197 }
198}
199
200pub struct RealGitRepository {
201 pub repository: Mutex<git2::Repository>,
202 pub git_binary_path: PathBuf,
203 hosting_provider_registry: Arc<GitHostingProviderRegistry>,
204}
205
206impl RealGitRepository {
207 pub fn new(
208 repository: git2::Repository,
209 git_binary_path: Option<PathBuf>,
210 hosting_provider_registry: Arc<GitHostingProviderRegistry>,
211 ) -> Self {
212 Self {
213 repository: Mutex::new(repository),
214 git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
215 hosting_provider_registry,
216 }
217 }
218
219 fn working_directory(&self) -> Result<PathBuf> {
220 self.repository
221 .lock()
222 .workdir()
223 .context("failed to read git work directory")
224 .map(Path::to_path_buf)
225 }
226}
227
228// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
229const GIT_MODE_SYMLINK: u32 = 0o120000;
230
231impl GitRepository for RealGitRepository {
232 fn reload_index(&self) {
233 if let Ok(mut index) = self.repository.lock().index() {
234 _ = index.read(false);
235 }
236 }
237
238 fn path(&self) -> PathBuf {
239 let repo = self.repository.lock();
240 repo.path().into()
241 }
242
243 fn main_repository_path(&self) -> PathBuf {
244 let repo = self.repository.lock();
245 repo.commondir().into()
246 }
247
248 fn show(&self, commit: &str) -> Result<CommitDetails> {
249 let repo = self.repository.lock();
250 let Ok(commit) = repo.revparse_single(commit)?.into_commit() else {
251 anyhow::bail!("{} is not a commit", commit);
252 };
253 let details = CommitDetails {
254 sha: commit.id().to_string().into(),
255 message: String::from_utf8_lossy(commit.message_raw_bytes())
256 .to_string()
257 .into(),
258 commit_timestamp: commit.time().seconds(),
259 committer_email: String::from_utf8_lossy(commit.committer().email_bytes())
260 .to_string()
261 .into(),
262 committer_name: String::from_utf8_lossy(commit.committer().name_bytes())
263 .to_string()
264 .into(),
265 };
266 Ok(details)
267 }
268
269 fn reset(&self, commit: &str, mode: ResetMode) -> Result<()> {
270 let working_directory = self.working_directory()?;
271
272 let mode_flag = match mode {
273 ResetMode::Mixed => "--mixed",
274 ResetMode::Soft => "--soft",
275 };
276
277 let output = new_std_command(&self.git_binary_path)
278 .current_dir(&working_directory)
279 .args(["reset", mode_flag, commit])
280 .output()?;
281 if !output.status.success() {
282 return Err(anyhow!(
283 "Failed to reset:\n{}",
284 String::from_utf8_lossy(&output.stderr)
285 ));
286 }
287 Ok(())
288 }
289
290 fn checkout_files(&self, commit: &str, paths: &[RepoPath]) -> Result<()> {
291 if paths.is_empty() {
292 return Ok(());
293 }
294 let working_directory = self.working_directory()?;
295
296 let output = new_std_command(&self.git_binary_path)
297 .current_dir(&working_directory)
298 .args(["checkout", commit, "--"])
299 .args(paths.iter().map(|path| path.as_ref()))
300 .output()?;
301 if !output.status.success() {
302 return Err(anyhow!(
303 "Failed to checkout files:\n{}",
304 String::from_utf8_lossy(&output.stderr)
305 ));
306 }
307 Ok(())
308 }
309
310 fn load_index_text(&self, path: &RepoPath) -> Option<String> {
311 fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
312 const STAGE_NORMAL: i32 = 0;
313 let index = repo.index()?;
314
315 // This check is required because index.get_path() unwraps internally :(
316 check_path_to_repo_path_errors(path)?;
317
318 let oid = match index.get_path(path, STAGE_NORMAL) {
319 Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
320 _ => return Ok(None),
321 };
322
323 let content = repo.find_blob(oid)?.content().to_owned();
324 Ok(Some(String::from_utf8(content)?))
325 }
326
327 match logic(&self.repository.lock(), path) {
328 Ok(value) => return value,
329 Err(err) => log::error!("Error loading index text: {:?}", err),
330 }
331 None
332 }
333
334 fn load_committed_text(&self, path: &RepoPath) -> Option<String> {
335 let repo = self.repository.lock();
336 let head = repo.head().ok()?.peel_to_tree().log_err()?;
337 let entry = head.get_path(path).ok()?;
338 if entry.filemode() == i32::from(git2::FileMode::Link) {
339 return None;
340 }
341 let content = repo.find_blob(entry.id()).log_err()?.content().to_owned();
342 let content = String::from_utf8(content).log_err()?;
343 Some(content)
344 }
345
346 fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
347 let working_directory = self.working_directory()?;
348 if let Some(content) = content {
349 let mut child = new_std_command(&self.git_binary_path)
350 .current_dir(&working_directory)
351 .args(["hash-object", "-w", "--stdin"])
352 .stdin(Stdio::piped())
353 .stdout(Stdio::piped())
354 .spawn()?;
355 child.stdin.take().unwrap().write_all(content.as_bytes())?;
356 let output = child.wait_with_output()?.stdout;
357 let sha = String::from_utf8(output)?;
358
359 log::debug!("indexing SHA: {sha}, path {path:?}");
360
361 let output = new_std_command(&self.git_binary_path)
362 .current_dir(&working_directory)
363 .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
364 .arg(path.as_ref())
365 .output()?;
366
367 if !output.status.success() {
368 return Err(anyhow!(
369 "Failed to stage:\n{}",
370 String::from_utf8_lossy(&output.stderr)
371 ));
372 }
373 } else {
374 let output = new_std_command(&self.git_binary_path)
375 .current_dir(&working_directory)
376 .args(["update-index", "--force-remove"])
377 .arg(path.as_ref())
378 .output()?;
379
380 if !output.status.success() {
381 return Err(anyhow!(
382 "Failed to unstage:\n{}",
383 String::from_utf8_lossy(&output.stderr)
384 ));
385 }
386 }
387
388 Ok(())
389 }
390
391 fn remote_url(&self, name: &str) -> Option<String> {
392 let repo = self.repository.lock();
393 let remote = repo.find_remote(name).ok()?;
394 remote.url().map(|url| url.to_string())
395 }
396
397 fn head_sha(&self) -> Option<String> {
398 Some(self.repository.lock().head().ok()?.target()?.to_string())
399 }
400
401 fn merge_head_shas(&self) -> Vec<String> {
402 let mut shas = Vec::default();
403 self.repository
404 .lock()
405 .mergehead_foreach(|oid| {
406 shas.push(oid.to_string());
407 true
408 })
409 .ok();
410 shas
411 }
412
413 fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
414 let working_directory = self
415 .repository
416 .lock()
417 .workdir()
418 .context("failed to read git work directory")?
419 .to_path_buf();
420 GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes)
421 }
422
423 fn branch_exits(&self, name: &str) -> Result<bool> {
424 let repo = self.repository.lock();
425 let branch = repo.find_branch(name, BranchType::Local);
426 match branch {
427 Ok(_) => Ok(true),
428 Err(e) => match e.code() {
429 git2::ErrorCode::NotFound => Ok(false),
430 _ => Err(anyhow!(e)),
431 },
432 }
433 }
434
435 fn branches(&self) -> Result<Vec<Branch>> {
436 let working_directory = self
437 .repository
438 .lock()
439 .workdir()
440 .context("failed to read git work directory")?
441 .to_path_buf();
442 let fields = [
443 "%(HEAD)",
444 "%(objectname)",
445 "%(refname)",
446 "%(upstream)",
447 "%(upstream:track)",
448 "%(committerdate:unix)",
449 "%(contents:subject)",
450 ]
451 .join("%00");
452 let args = vec!["for-each-ref", "refs/heads/**/*", "--format", &fields];
453
454 let output = new_std_command(&self.git_binary_path)
455 .current_dir(&working_directory)
456 .args(args)
457 .output()?;
458
459 if !output.status.success() {
460 return Err(anyhow!(
461 "Failed to git git branches:\n{}",
462 String::from_utf8_lossy(&output.stderr)
463 ));
464 }
465
466 let input = String::from_utf8_lossy(&output.stdout);
467
468 let mut branches = parse_branch_input(&input)?;
469 if branches.is_empty() {
470 let args = vec!["symbolic-ref", "--quiet", "--short", "HEAD"];
471
472 let output = new_std_command(&self.git_binary_path)
473 .current_dir(&working_directory)
474 .args(args)
475 .output()?;
476
477 // git symbolic-ref returns a non-0 exit code if HEAD points
478 // to something other than a branch
479 if output.status.success() {
480 let name = String::from_utf8_lossy(&output.stdout).trim().to_string();
481
482 branches.push(Branch {
483 name: name.into(),
484 is_head: true,
485 upstream: None,
486 most_recent_commit: None,
487 });
488 }
489 }
490
491 Ok(branches)
492 }
493
494 fn change_branch(&self, name: &str) -> Result<()> {
495 let repo = self.repository.lock();
496 let revision = repo.find_branch(name, BranchType::Local)?;
497 let revision = revision.get();
498 let as_tree = revision.peel_to_tree()?;
499 repo.checkout_tree(as_tree.as_object(), None)?;
500 repo.set_head(
501 revision
502 .name()
503 .ok_or_else(|| anyhow!("Branch name could not be retrieved"))?,
504 )?;
505 Ok(())
506 }
507
508 fn create_branch(&self, name: &str) -> Result<()> {
509 let repo = self.repository.lock();
510 let current_commit = repo.head()?.peel_to_commit()?;
511 repo.branch(name, ¤t_commit, false)?;
512 Ok(())
513 }
514
515 fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame> {
516 let working_directory = self
517 .repository
518 .lock()
519 .workdir()
520 .with_context(|| format!("failed to get git working directory for file {:?}", path))?
521 .to_path_buf();
522
523 const REMOTE_NAME: &str = "origin";
524 let remote_url = self.remote_url(REMOTE_NAME);
525
526 crate::blame::Blame::for_path(
527 &self.git_binary_path,
528 &working_directory,
529 path,
530 &content,
531 remote_url,
532 self.hosting_provider_registry.clone(),
533 )
534 }
535
536 fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
537 let working_directory = self.working_directory()?;
538
539 if !paths.is_empty() {
540 let output = new_std_command(&self.git_binary_path)
541 .current_dir(&working_directory)
542 .args(["update-index", "--add", "--remove", "--"])
543 .args(paths.iter().map(|p| p.as_ref()))
544 .output()?;
545
546 // TODO: Get remote response out of this and show it to the user
547 if !output.status.success() {
548 return Err(anyhow!(
549 "Failed to stage paths:\n{}",
550 String::from_utf8_lossy(&output.stderr)
551 ));
552 }
553 }
554 Ok(())
555 }
556
557 fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
558 let working_directory = self.working_directory()?;
559
560 if !paths.is_empty() {
561 let output = new_std_command(&self.git_binary_path)
562 .current_dir(&working_directory)
563 .args(["reset", "--quiet", "--"])
564 .args(paths.iter().map(|p| p.as_ref()))
565 .output()?;
566
567 // TODO: Get remote response out of this and show it to the user
568 if !output.status.success() {
569 return Err(anyhow!(
570 "Failed to unstage:\n{}",
571 String::from_utf8_lossy(&output.stderr)
572 ));
573 }
574 }
575 Ok(())
576 }
577
578 fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
579 let working_directory = self.working_directory()?;
580
581 let mut cmd = new_std_command(&self.git_binary_path);
582 cmd.current_dir(&working_directory)
583 .args(["commit", "--quiet", "-m"])
584 .arg(message)
585 .arg("--cleanup=strip");
586
587 if let Some((name, email)) = name_and_email {
588 cmd.arg("--author").arg(&format!("{name} <{email}>"));
589 }
590
591 let output = cmd.output()?;
592
593 // TODO: Get remote response out of this and show it to the user
594 if !output.status.success() {
595 return Err(anyhow!(
596 "Failed to commit:\n{}",
597 String::from_utf8_lossy(&output.stderr)
598 ));
599 }
600 Ok(())
601 }
602
603 fn push(
604 &self,
605 branch_name: &str,
606 remote_name: &str,
607 options: Option<PushOptions>,
608 ) -> Result<()> {
609 let working_directory = self.working_directory()?;
610
611 let output = new_std_command(&self.git_binary_path)
612 .current_dir(&working_directory)
613 .args(["push", "--quiet"])
614 .args(options.map(|option| match option {
615 PushOptions::SetUpstream => "--set-upstream",
616 PushOptions::Force => "--force-with-lease",
617 }))
618 .arg(remote_name)
619 .arg(format!("{}:{}", branch_name, branch_name))
620 .output()?;
621
622 if !output.status.success() {
623 return Err(anyhow!(
624 "Failed to push:\n{}",
625 String::from_utf8_lossy(&output.stderr)
626 ));
627 } else {
628 Ok(())
629 }
630 }
631
632 fn pull(&self, branch_name: &str, remote_name: &str) -> Result<()> {
633 let working_directory = self.working_directory()?;
634
635 let output = new_std_command(&self.git_binary_path)
636 .current_dir(&working_directory)
637 .args(["pull", "--quiet"])
638 .arg(remote_name)
639 .arg(branch_name)
640 .output()?;
641
642 if !output.status.success() {
643 return Err(anyhow!(
644 "Failed to pull:\n{}",
645 String::from_utf8_lossy(&output.stderr)
646 ));
647 } else {
648 return Ok(());
649 }
650 }
651
652 fn fetch(&self) -> Result<()> {
653 let working_directory = self.working_directory()?;
654
655 let output = new_std_command(&self.git_binary_path)
656 .current_dir(&working_directory)
657 .args(["fetch", "--quiet", "--all"])
658 .output()?;
659
660 if !output.status.success() {
661 return Err(anyhow!(
662 "Failed to fetch:\n{}",
663 String::from_utf8_lossy(&output.stderr)
664 ));
665 } else {
666 return Ok(());
667 }
668 }
669
670 fn get_remotes(&self, branch_name: Option<&str>) -> Result<Vec<Remote>> {
671 let working_directory = self.working_directory()?;
672
673 if let Some(branch_name) = branch_name {
674 let output = new_std_command(&self.git_binary_path)
675 .current_dir(&working_directory)
676 .args(["config", "--get"])
677 .arg(format!("branch.{}.remote", branch_name))
678 .output()?;
679
680 if output.status.success() {
681 let remote_name = String::from_utf8_lossy(&output.stdout);
682
683 return Ok(vec![Remote {
684 name: remote_name.trim().to_string().into(),
685 }]);
686 }
687 }
688
689 let output = new_std_command(&self.git_binary_path)
690 .current_dir(&working_directory)
691 .args(["remote"])
692 .output()?;
693
694 if output.status.success() {
695 let remote_names = String::from_utf8_lossy(&output.stdout)
696 .split('\n')
697 .filter(|name| !name.is_empty())
698 .map(|name| Remote {
699 name: name.trim().to_string().into(),
700 })
701 .collect();
702
703 return Ok(remote_names);
704 } else {
705 return Err(anyhow!(
706 "Failed to get remotes:\n{}",
707 String::from_utf8_lossy(&output.stderr)
708 ));
709 }
710 }
711}
712
713#[derive(Debug, Clone)]
714pub struct FakeGitRepository {
715 state: Arc<Mutex<FakeGitRepositoryState>>,
716}
717
718#[derive(Debug, Clone)]
719pub struct FakeGitRepositoryState {
720 pub path: PathBuf,
721 pub event_emitter: smol::channel::Sender<PathBuf>,
722 pub head_contents: HashMap<RepoPath, String>,
723 pub index_contents: HashMap<RepoPath, String>,
724 pub blames: HashMap<RepoPath, Blame>,
725 pub statuses: HashMap<RepoPath, FileStatus>,
726 pub current_branch_name: Option<String>,
727 pub branches: HashSet<String>,
728}
729
730impl FakeGitRepository {
731 pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
732 Arc::new(FakeGitRepository { state })
733 }
734}
735
736impl FakeGitRepositoryState {
737 pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
738 FakeGitRepositoryState {
739 path,
740 event_emitter,
741 head_contents: Default::default(),
742 index_contents: Default::default(),
743 blames: Default::default(),
744 statuses: Default::default(),
745 current_branch_name: Default::default(),
746 branches: Default::default(),
747 }
748 }
749}
750
751impl GitRepository for FakeGitRepository {
752 fn reload_index(&self) {}
753
754 fn load_index_text(&self, path: &RepoPath) -> Option<String> {
755 let state = self.state.lock();
756 state.index_contents.get(path.as_ref()).cloned()
757 }
758
759 fn load_committed_text(&self, path: &RepoPath) -> Option<String> {
760 let state = self.state.lock();
761 state.head_contents.get(path.as_ref()).cloned()
762 }
763
764 fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
765 let mut state = self.state.lock();
766 if let Some(content) = content {
767 state.index_contents.insert(path.clone(), content);
768 } else {
769 state.index_contents.remove(path);
770 }
771 state
772 .event_emitter
773 .try_send(state.path.clone())
774 .expect("Dropped repo change event");
775 Ok(())
776 }
777
778 fn remote_url(&self, _name: &str) -> Option<String> {
779 None
780 }
781
782 fn head_sha(&self) -> Option<String> {
783 None
784 }
785
786 fn merge_head_shas(&self) -> Vec<String> {
787 vec![]
788 }
789
790 fn show(&self, _: &str) -> Result<CommitDetails> {
791 unimplemented!()
792 }
793
794 fn reset(&self, _: &str, _: ResetMode) -> Result<()> {
795 unimplemented!()
796 }
797
798 fn checkout_files(&self, _: &str, _: &[RepoPath]) -> Result<()> {
799 unimplemented!()
800 }
801
802 fn path(&self) -> PathBuf {
803 let state = self.state.lock();
804 state.path.clone()
805 }
806
807 fn main_repository_path(&self) -> PathBuf {
808 self.path()
809 }
810
811 fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
812 let state = self.state.lock();
813
814 let mut entries = state
815 .statuses
816 .iter()
817 .filter_map(|(repo_path, status)| {
818 if path_prefixes
819 .iter()
820 .any(|path_prefix| repo_path.0.starts_with(path_prefix))
821 {
822 Some((repo_path.to_owned(), *status))
823 } else {
824 None
825 }
826 })
827 .collect::<Vec<_>>();
828 entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
829
830 Ok(GitStatus {
831 entries: entries.into(),
832 })
833 }
834
835 fn branches(&self) -> Result<Vec<Branch>> {
836 let state = self.state.lock();
837 let current_branch = &state.current_branch_name;
838 Ok(state
839 .branches
840 .iter()
841 .map(|branch_name| Branch {
842 is_head: Some(branch_name) == current_branch.as_ref(),
843 name: branch_name.into(),
844 most_recent_commit: None,
845 upstream: None,
846 })
847 .collect())
848 }
849
850 fn branch_exits(&self, name: &str) -> Result<bool> {
851 let state = self.state.lock();
852 Ok(state.branches.contains(name))
853 }
854
855 fn change_branch(&self, name: &str) -> Result<()> {
856 let mut state = self.state.lock();
857 state.current_branch_name = Some(name.to_owned());
858 state
859 .event_emitter
860 .try_send(state.path.clone())
861 .expect("Dropped repo change event");
862 Ok(())
863 }
864
865 fn create_branch(&self, name: &str) -> Result<()> {
866 let mut state = self.state.lock();
867 state.branches.insert(name.to_owned());
868 state
869 .event_emitter
870 .try_send(state.path.clone())
871 .expect("Dropped repo change event");
872 Ok(())
873 }
874
875 fn blame(&self, path: &Path, _content: Rope) -> Result<crate::blame::Blame> {
876 let state = self.state.lock();
877 state
878 .blames
879 .get(path)
880 .with_context(|| format!("failed to get blame for {:?}", path))
881 .cloned()
882 }
883
884 fn stage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
885 unimplemented!()
886 }
887
888 fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
889 unimplemented!()
890 }
891
892 fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
893 unimplemented!()
894 }
895
896 fn push(&self, _branch: &str, _remote: &str, _options: Option<PushOptions>) -> Result<()> {
897 unimplemented!()
898 }
899
900 fn pull(&self, _branch: &str, _remote: &str) -> Result<()> {
901 unimplemented!()
902 }
903
904 fn fetch(&self) -> Result<()> {
905 unimplemented!()
906 }
907
908 fn get_remotes(&self, _branch: Option<&str>) -> Result<Vec<Remote>> {
909 unimplemented!()
910 }
911}
912
913fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
914 match relative_file_path.components().next() {
915 None => anyhow::bail!("repo path should not be empty"),
916 Some(Component::Prefix(_)) => anyhow::bail!(
917 "repo path `{}` should be relative, not a windows prefix",
918 relative_file_path.to_string_lossy()
919 ),
920 Some(Component::RootDir) => {
921 anyhow::bail!(
922 "repo path `{}` should be relative",
923 relative_file_path.to_string_lossy()
924 )
925 }
926 Some(Component::CurDir) => {
927 anyhow::bail!(
928 "repo path `{}` should not start with `.`",
929 relative_file_path.to_string_lossy()
930 )
931 }
932 Some(Component::ParentDir) => {
933 anyhow::bail!(
934 "repo path `{}` should not start with `..`",
935 relative_file_path.to_string_lossy()
936 )
937 }
938 _ => Ok(()),
939 }
940}
941
942pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
943 LazyLock::new(|| RepoPath(Path::new("").into()));
944
945#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
946pub struct RepoPath(pub Arc<Path>);
947
948impl RepoPath {
949 pub fn new(path: PathBuf) -> Self {
950 debug_assert!(path.is_relative(), "Repo paths must be relative");
951
952 RepoPath(path.into())
953 }
954
955 pub fn from_str(path: &str) -> Self {
956 let path = Path::new(path);
957 debug_assert!(path.is_relative(), "Repo paths must be relative");
958
959 RepoPath(path.into())
960 }
961}
962
963impl std::fmt::Display for RepoPath {
964 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
965 self.0.to_string_lossy().fmt(f)
966 }
967}
968
969impl From<&Path> for RepoPath {
970 fn from(value: &Path) -> Self {
971 RepoPath::new(value.into())
972 }
973}
974
975impl From<Arc<Path>> for RepoPath {
976 fn from(value: Arc<Path>) -> Self {
977 RepoPath(value)
978 }
979}
980
981impl From<PathBuf> for RepoPath {
982 fn from(value: PathBuf) -> Self {
983 RepoPath::new(value)
984 }
985}
986
987impl From<&str> for RepoPath {
988 fn from(value: &str) -> Self {
989 Self::from_str(value)
990 }
991}
992
993impl Default for RepoPath {
994 fn default() -> Self {
995 RepoPath(Path::new("").into())
996 }
997}
998
999impl AsRef<Path> for RepoPath {
1000 fn as_ref(&self) -> &Path {
1001 self.0.as_ref()
1002 }
1003}
1004
1005impl std::ops::Deref for RepoPath {
1006 type Target = Path;
1007
1008 fn deref(&self) -> &Self::Target {
1009 &self.0
1010 }
1011}
1012
1013impl Borrow<Path> for RepoPath {
1014 fn borrow(&self) -> &Path {
1015 self.0.as_ref()
1016 }
1017}
1018
1019#[derive(Debug)]
1020pub struct RepoPathDescendants<'a>(pub &'a Path);
1021
1022impl MapSeekTarget<RepoPath> for RepoPathDescendants<'_> {
1023 fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
1024 if key.starts_with(self.0) {
1025 Ordering::Greater
1026 } else {
1027 self.0.cmp(key)
1028 }
1029 }
1030}
1031
1032fn parse_branch_input(input: &str) -> Result<Vec<Branch>> {
1033 let mut branches = Vec::new();
1034 for line in input.split('\n') {
1035 if line.is_empty() {
1036 continue;
1037 }
1038 let mut fields = line.split('\x00');
1039 let is_current_branch = fields.next().context("no HEAD")? == "*";
1040 let head_sha: SharedString = fields.next().context("no objectname")?.to_string().into();
1041 let ref_name: SharedString = fields
1042 .next()
1043 .context("no refname")?
1044 .strip_prefix("refs/heads/")
1045 .context("unexpected format for refname")?
1046 .to_string()
1047 .into();
1048 let upstream_name = fields.next().context("no upstream")?.to_string();
1049 let upstream_tracking = parse_upstream_track(fields.next().context("no upstream:track")?)?;
1050 let commiterdate = fields.next().context("no committerdate")?.parse::<i64>()?;
1051 let subject: SharedString = fields
1052 .next()
1053 .context("no contents:subject")?
1054 .to_string()
1055 .into();
1056
1057 branches.push(Branch {
1058 is_head: is_current_branch,
1059 name: ref_name,
1060 most_recent_commit: Some(CommitSummary {
1061 sha: head_sha,
1062 subject,
1063 commit_timestamp: commiterdate,
1064 }),
1065 upstream: if upstream_name.is_empty() {
1066 None
1067 } else {
1068 Some(Upstream {
1069 ref_name: upstream_name.into(),
1070 tracking: upstream_tracking,
1071 })
1072 },
1073 })
1074 }
1075
1076 Ok(branches)
1077}
1078
1079fn parse_upstream_track(upstream_track: &str) -> Result<UpstreamTracking> {
1080 if upstream_track == "" {
1081 return Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
1082 ahead: 0,
1083 behind: 0,
1084 }));
1085 }
1086
1087 let upstream_track = upstream_track
1088 .strip_prefix("[")
1089 .ok_or_else(|| anyhow!("missing ["))?;
1090 let upstream_track = upstream_track
1091 .strip_suffix("]")
1092 .ok_or_else(|| anyhow!("missing ["))?;
1093 let mut ahead: u32 = 0;
1094 let mut behind: u32 = 0;
1095 for component in upstream_track.split(", ") {
1096 if component == "gone" {
1097 return Ok(UpstreamTracking::Gone);
1098 }
1099 if let Some(ahead_num) = component.strip_prefix("ahead ") {
1100 ahead = ahead_num.parse::<u32>()?;
1101 }
1102 if let Some(behind_num) = component.strip_prefix("behind ") {
1103 behind = behind_num.parse::<u32>()?;
1104 }
1105 }
1106 Ok(UpstreamTracking::Tracked(UpstreamTrackingStatus {
1107 ahead,
1108 behind,
1109 }))
1110}
1111
1112#[test]
1113fn test_branches_parsing() {
1114 // suppress "help: octal escapes are not supported, `\0` is always null"
1115 #[allow(clippy::octal_escapes)]
1116 let input = "*\0060964da10574cd9bf06463a53bf6e0769c5c45e\0refs/heads/zed-patches\0refs/remotes/origin/zed-patches\0\01733187470\0generated protobuf\n";
1117 assert_eq!(
1118 parse_branch_input(&input).unwrap(),
1119 vec![Branch {
1120 is_head: true,
1121 name: "zed-patches".into(),
1122 upstream: Some(Upstream {
1123 ref_name: "refs/remotes/origin/zed-patches".into(),
1124 tracking: UpstreamTracking::Tracked(UpstreamTrackingStatus {
1125 ahead: 0,
1126 behind: 0
1127 })
1128 }),
1129 most_recent_commit: Some(CommitSummary {
1130 sha: "060964da10574cd9bf06463a53bf6e0769c5c45e".into(),
1131 subject: "generated protobuf".into(),
1132 commit_timestamp: 1733187470,
1133 })
1134 }]
1135 )
1136}