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