1use anyhow::Result;
2use collections::HashMap;
3use git2::Repository as LibGitRepository;
4use parking_lot::Mutex;
5use std::{
6 path::{Path, PathBuf},
7 sync::Arc,
8};
9use util::ResultExt;
10
11#[async_trait::async_trait]
12pub trait GitRepository: Send + Sync + std::fmt::Debug {
13 fn manages(&self, path: &Path) -> bool;
14
15 fn in_dot_git(&self, path: &Path) -> bool;
16
17 fn content_path(&self) -> &Path;
18
19 fn git_dir_path(&self) -> &Path;
20
21 fn scan_id(&self) -> usize;
22
23 fn set_scan_id(&mut self, scan_id: usize);
24
25 fn reopen_git_repo(&mut self) -> bool;
26
27 fn git_repo(&self) -> Arc<Mutex<LibGitRepository>>;
28
29 fn boxed_clone(&self) -> Box<dyn GitRepository>;
30
31 async fn load_head_text(&self, relative_file_path: &Path) -> Option<String>;
32}
33
34#[derive(Clone)]
35pub struct RealGitRepository {
36 // Path to folder containing the .git file or directory
37 content_path: Arc<Path>,
38 // Path to the actual .git folder.
39 // Note: if .git is a file, this points to the folder indicated by the .git file
40 git_dir_path: Arc<Path>,
41 scan_id: usize,
42 libgit_repository: Arc<Mutex<LibGitRepository>>,
43}
44
45impl RealGitRepository {
46 pub fn open(dotgit_path: &Path) -> Option<Box<dyn GitRepository>> {
47 LibGitRepository::open(&dotgit_path)
48 .log_err()
49 .and_then::<Box<dyn GitRepository>, _>(|libgit_repository| {
50 Some(Box::new(Self {
51 content_path: libgit_repository.workdir()?.into(),
52 git_dir_path: dotgit_path.canonicalize().log_err()?.into(),
53 scan_id: 0,
54 libgit_repository: Arc::new(parking_lot::Mutex::new(libgit_repository)),
55 }))
56 })
57 }
58}
59
60#[async_trait::async_trait]
61impl GitRepository for RealGitRepository {
62 fn manages(&self, path: &Path) -> bool {
63 path.canonicalize()
64 .map(|path| path.starts_with(&self.content_path))
65 .unwrap_or(false)
66 }
67
68 fn in_dot_git(&self, path: &Path) -> bool {
69 path.canonicalize()
70 .map(|path| path.starts_with(&self.git_dir_path))
71 .unwrap_or(false)
72 }
73
74 fn content_path(&self) -> &Path {
75 self.content_path.as_ref()
76 }
77
78 fn git_dir_path(&self) -> &Path {
79 self.git_dir_path.as_ref()
80 }
81
82 fn scan_id(&self) -> usize {
83 self.scan_id
84 }
85
86 async fn load_head_text(&self, relative_file_path: &Path) -> Option<String> {
87 fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
88 const STAGE_NORMAL: i32 = 0;
89 let index = repo.index()?;
90 let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
91 Some(entry) => entry.id,
92 None => return Ok(None),
93 };
94
95 let content = repo.find_blob(oid)?.content().to_owned();
96 let head_text = String::from_utf8(content)?;
97 Ok(Some(head_text))
98 }
99
100 match logic(&self.libgit_repository.as_ref().lock(), relative_file_path) {
101 Ok(value) => return value,
102 Err(err) => log::error!("Error loading head text: {:?}", err),
103 }
104 None
105 }
106
107 fn reopen_git_repo(&mut self) -> bool {
108 match LibGitRepository::open(&self.git_dir_path) {
109 Ok(repo) => {
110 self.libgit_repository = Arc::new(Mutex::new(repo));
111 true
112 }
113
114 Err(_) => false,
115 }
116 }
117
118 fn git_repo(&self) -> Arc<Mutex<LibGitRepository>> {
119 self.libgit_repository.clone()
120 }
121
122 fn set_scan_id(&mut self, scan_id: usize) {
123 self.scan_id = scan_id;
124 }
125
126 fn boxed_clone(&self) -> Box<dyn GitRepository> {
127 Box::new(self.clone())
128 }
129}
130
131impl std::fmt::Debug for RealGitRepository {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.debug_struct("GitRepository")
134 .field("content_path", &self.content_path)
135 .field("git_dir_path", &self.git_dir_path)
136 .field("scan_id", &self.scan_id)
137 .field("libgit_repository", &"LibGitRepository")
138 .finish()
139 }
140}
141
142#[derive(Debug, Clone)]
143pub struct FakeGitRepository {
144 content_path: Arc<Path>,
145 git_dir_path: Arc<Path>,
146 scan_id: usize,
147 state: Arc<Mutex<FakeGitRepositoryState>>,
148}
149
150#[derive(Debug, Clone, Default)]
151pub struct FakeGitRepositoryState {
152 pub index_contents: HashMap<PathBuf, String>,
153}
154
155impl FakeGitRepository {
156 pub fn open(
157 dotgit_path: &Path,
158 scan_id: usize,
159 state: Arc<Mutex<FakeGitRepositoryState>>,
160 ) -> Box<dyn GitRepository> {
161 Box::new(FakeGitRepository {
162 content_path: dotgit_path.parent().unwrap().into(),
163 git_dir_path: dotgit_path.into(),
164 scan_id,
165 state,
166 })
167 }
168}
169
170#[async_trait::async_trait]
171impl GitRepository for FakeGitRepository {
172 fn manages(&self, path: &Path) -> bool {
173 path.starts_with(self.content_path())
174 }
175
176 fn in_dot_git(&self, path: &Path) -> bool {
177 path.starts_with(self.git_dir_path())
178 }
179
180 fn content_path(&self) -> &Path {
181 &self.content_path
182 }
183
184 fn git_dir_path(&self) -> &Path {
185 &self.git_dir_path
186 }
187
188 fn scan_id(&self) -> usize {
189 self.scan_id
190 }
191
192 async fn load_head_text(&self, path: &Path) -> Option<String> {
193 let state = self.state.lock();
194 state.index_contents.get(path).cloned()
195 }
196
197 fn reopen_git_repo(&mut self) -> bool {
198 true
199 }
200
201 fn git_repo(&self) -> Arc<Mutex<LibGitRepository>> {
202 unimplemented!()
203 }
204
205 fn set_scan_id(&mut self, scan_id: usize) {
206 self.scan_id = scan_id;
207 }
208
209 fn boxed_clone(&self) -> Box<dyn GitRepository> {
210 Box::new(self.clone())
211 }
212}