1use anyhow::Result;
2use collections::HashMap;
3use parking_lot::Mutex;
4use std::{
5 path::{Component, Path, PathBuf},
6 sync::Arc,
7};
8use util::ResultExt;
9
10pub use git2::Repository as LibGitRepository;
11
12#[async_trait::async_trait]
13pub trait GitRepository: Send {
14 fn reload_index(&self);
15
16 fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
17
18 fn branch_name(&self) -> Option<String>;
19}
20
21impl std::fmt::Debug for dyn GitRepository {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 f.debug_struct("dyn GitRepository<...>").finish()
24 }
25}
26
27#[async_trait::async_trait]
28impl GitRepository for LibGitRepository {
29 fn reload_index(&self) {
30 if let Ok(mut index) = self.index() {
31 _ = index.read(false);
32 }
33 }
34
35 fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
36 fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
37 const STAGE_NORMAL: i32 = 0;
38 let index = repo.index()?;
39
40 // This check is required because index.get_path() unwraps internally :(
41 check_path_to_repo_path_errors(relative_file_path)?;
42
43 let oid = match index.get_path(&relative_file_path, STAGE_NORMAL) {
44 Some(entry) => entry.id,
45 None => return Ok(None),
46 };
47
48 let content = repo.find_blob(oid)?.content().to_owned();
49 Ok(Some(String::from_utf8(content)?))
50 }
51
52 match logic(&self, relative_file_path) {
53 Ok(value) => return value,
54 Err(err) => log::error!("Error loading head text: {:?}", err),
55 }
56 None
57 }
58
59 fn branch_name(&self) -> Option<String> {
60 let head = self.head().log_err()?;
61 let branch = String::from_utf8_lossy(head.shorthand_bytes());
62 Some(branch.to_string())
63 }
64}
65
66#[derive(Debug, Clone, Default)]
67pub struct FakeGitRepository {
68 state: Arc<Mutex<FakeGitRepositoryState>>,
69}
70
71#[derive(Debug, Clone, Default)]
72pub struct FakeGitRepositoryState {
73 pub index_contents: HashMap<PathBuf, String>,
74 pub branch_name: Option<String>,
75}
76
77impl FakeGitRepository {
78 pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
79 Arc::new(Mutex::new(FakeGitRepository { state }))
80 }
81}
82
83#[async_trait::async_trait]
84impl GitRepository for FakeGitRepository {
85 fn reload_index(&self) {}
86
87 fn load_index_text(&self, path: &Path) -> Option<String> {
88 let state = self.state.lock();
89 state.index_contents.get(path).cloned()
90 }
91
92 fn branch_name(&self) -> Option<String> {
93 let state = self.state.lock();
94 state.branch_name.clone()
95 }
96}
97
98fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
99 match relative_file_path.components().next() {
100 None => anyhow::bail!("repo path should not be empty"),
101 Some(Component::Prefix(_)) => anyhow::bail!(
102 "repo path `{}` should be relative, not a windows prefix",
103 relative_file_path.to_string_lossy()
104 ),
105 Some(Component::RootDir) => {
106 anyhow::bail!(
107 "repo path `{}` should be relative",
108 relative_file_path.to_string_lossy()
109 )
110 }
111 Some(Component::CurDir) => {
112 anyhow::bail!(
113 "repo path `{}` should not start with `.`",
114 relative_file_path.to_string_lossy()
115 )
116 }
117 Some(Component::ParentDir) => {
118 anyhow::bail!(
119 "repo path `{}` should not start with `..`",
120 relative_file_path.to_string_lossy()
121 )
122 }
123 _ => Ok(()),
124 }
125}