1use crate::GitHostingProviderRegistry;
2use crate::{blame::Blame, status::GitStatus};
3use anyhow::{Context, Result};
4use collections::HashMap;
5use git2::BranchType;
6use parking_lot::Mutex;
7use rope::Rope;
8use serde::{Deserialize, Serialize};
9use std::{
10 cmp::Ordering,
11 path::{Component, Path, PathBuf},
12 sync::Arc,
13};
14use sum_tree::MapSeekTarget;
15use util::ResultExt;
16
17pub use git2::Repository as LibGitRepository;
18
19#[derive(Clone, Debug, Hash, PartialEq)]
20pub struct Branch {
21 pub is_head: bool,
22 pub name: Box<str>,
23 /// Timestamp of most recent commit, normalized to Unix Epoch format.
24 pub unix_timestamp: Option<i64>,
25}
26
27pub trait GitRepository: Send {
28 fn reload_index(&self);
29
30 /// Loads a git repository entry's contents.
31 /// Note that for symlink entries, this will return the contents of the symlink, not the target.
32 fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
33
34 /// Returns the URL of the remote with the given name.
35 fn remote_url(&self, name: &str) -> Option<String>;
36 fn branch_name(&self) -> Option<String>;
37
38 /// Returns the SHA of the current HEAD.
39 fn head_sha(&self) -> Option<String>;
40
41 fn statuses(&self, path_prefix: &Path) -> Result<GitStatus>;
42
43 fn status(&self, path: &Path) -> Option<GitFileStatus> {
44 Some(self.statuses(path).ok()?.entries.first()?.1)
45 }
46
47 fn branches(&self) -> Result<Vec<Branch>>;
48 fn change_branch(&self, _: &str) -> Result<()>;
49 fn create_branch(&self, _: &str) -> Result<()>;
50
51 fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
52}
53
54impl std::fmt::Debug for dyn GitRepository {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 f.debug_struct("dyn GitRepository<...>").finish()
57 }
58}
59
60pub struct RealGitRepository {
61 pub repository: LibGitRepository,
62 pub git_binary_path: PathBuf,
63 hosting_provider_registry: Arc<GitHostingProviderRegistry>,
64}
65
66impl RealGitRepository {
67 pub fn new(
68 repository: LibGitRepository,
69 git_binary_path: Option<PathBuf>,
70 hosting_provider_registry: Arc<GitHostingProviderRegistry>,
71 ) -> Self {
72 Self {
73 repository,
74 git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
75 hosting_provider_registry,
76 }
77 }
78}
79
80impl GitRepository for RealGitRepository {
81 fn reload_index(&self) {
82 if let Ok(mut index) = self.repository.index() {
83 _ = index.read(false);
84 }
85 }
86
87 fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
88 fn logic(repo: &LibGitRepository, relative_file_path: &Path) -> Result<Option<String>> {
89 const STAGE_NORMAL: i32 = 0;
90 let index = repo.index()?;
91
92 // This check is required because index.get_path() unwraps internally :(
93 check_path_to_repo_path_errors(relative_file_path)?;
94
95 let oid = match index.get_path(relative_file_path, STAGE_NORMAL) {
96 Some(entry) => entry.id,
97 None => return Ok(None),
98 };
99
100 let content = repo.find_blob(oid)?.content().to_owned();
101 Ok(Some(String::from_utf8(content)?))
102 }
103
104 match logic(&self.repository, relative_file_path) {
105 Ok(value) => return value,
106 Err(err) => log::error!("Error loading head text: {:?}", err),
107 }
108 None
109 }
110
111 fn remote_url(&self, name: &str) -> Option<String> {
112 let remote = self.repository.find_remote(name).ok()?;
113 remote.url().map(|url| url.to_string())
114 }
115
116 fn branch_name(&self) -> Option<String> {
117 let head = self.repository.head().log_err()?;
118 let branch = String::from_utf8_lossy(head.shorthand_bytes());
119 Some(branch.to_string())
120 }
121
122 fn head_sha(&self) -> Option<String> {
123 let head = self.repository.head().ok()?;
124 head.target().map(|oid| oid.to_string())
125 }
126
127 fn statuses(&self, path_prefix: &Path) -> Result<GitStatus> {
128 let working_directory = self
129 .repository
130 .workdir()
131 .context("failed to read git work directory")?;
132 GitStatus::new(&self.git_binary_path, working_directory, path_prefix)
133 }
134
135 fn branches(&self) -> Result<Vec<Branch>> {
136 let local_branches = self.repository.branches(Some(BranchType::Local))?;
137 let valid_branches = local_branches
138 .filter_map(|branch| {
139 branch.ok().and_then(|(branch, _)| {
140 let is_head = branch.is_head();
141 let name = branch.name().ok().flatten().map(Box::from)?;
142 let timestamp = branch.get().peel_to_commit().ok()?.time();
143 let unix_timestamp = timestamp.seconds();
144 let timezone_offset = timestamp.offset_minutes();
145 let utc_offset =
146 time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
147 let unix_timestamp =
148 time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
149 Some(Branch {
150 is_head,
151 name,
152 unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
153 })
154 })
155 })
156 .collect();
157 Ok(valid_branches)
158 }
159
160 fn change_branch(&self, name: &str) -> Result<()> {
161 let revision = self.repository.find_branch(name, BranchType::Local)?;
162 let revision = revision.get();
163 let as_tree = revision.peel_to_tree()?;
164 self.repository.checkout_tree(as_tree.as_object(), None)?;
165 self.repository.set_head(
166 revision
167 .name()
168 .ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
169 )?;
170 Ok(())
171 }
172 fn create_branch(&self, name: &str) -> Result<()> {
173 let current_commit = self.repository.head()?.peel_to_commit()?;
174 self.repository.branch(name, ¤t_commit, false)?;
175
176 Ok(())
177 }
178
179 fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame> {
180 let working_directory = self
181 .repository
182 .workdir()
183 .with_context(|| format!("failed to get git working directory for file {:?}", path))?;
184
185 const REMOTE_NAME: &str = "origin";
186 let remote_url = self.remote_url(REMOTE_NAME);
187
188 crate::blame::Blame::for_path(
189 &self.git_binary_path,
190 working_directory,
191 path,
192 &content,
193 remote_url,
194 self.hosting_provider_registry.clone(),
195 )
196 }
197}
198
199#[derive(Debug, Clone, Default)]
200pub struct FakeGitRepository {
201 state: Arc<Mutex<FakeGitRepositoryState>>,
202}
203
204#[derive(Debug, Clone, Default)]
205pub struct FakeGitRepositoryState {
206 pub index_contents: HashMap<PathBuf, String>,
207 pub blames: HashMap<PathBuf, Blame>,
208 pub worktree_statuses: HashMap<RepoPath, GitFileStatus>,
209 pub branch_name: Option<String>,
210}
211
212impl FakeGitRepository {
213 pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<Mutex<dyn GitRepository>> {
214 Arc::new(Mutex::new(FakeGitRepository { state }))
215 }
216}
217
218impl GitRepository for FakeGitRepository {
219 fn reload_index(&self) {}
220
221 fn load_index_text(&self, path: &Path) -> Option<String> {
222 let state = self.state.lock();
223 state.index_contents.get(path).cloned()
224 }
225
226 fn remote_url(&self, _name: &str) -> Option<String> {
227 None
228 }
229
230 fn branch_name(&self) -> Option<String> {
231 let state = self.state.lock();
232 state.branch_name.clone()
233 }
234
235 fn head_sha(&self) -> Option<String> {
236 None
237 }
238
239 fn statuses(&self, path_prefix: &Path) -> Result<GitStatus> {
240 let state = self.state.lock();
241 let mut entries = state
242 .worktree_statuses
243 .iter()
244 .filter_map(|(repo_path, status)| {
245 if repo_path.0.starts_with(path_prefix) {
246 Some((repo_path.to_owned(), *status))
247 } else {
248 None
249 }
250 })
251 .collect::<Vec<_>>();
252 entries.sort_unstable_by(|a, b| a.0.cmp(&b.0));
253 Ok(GitStatus {
254 entries: entries.into(),
255 })
256 }
257
258 fn branches(&self) -> Result<Vec<Branch>> {
259 Ok(vec![])
260 }
261
262 fn change_branch(&self, name: &str) -> Result<()> {
263 let mut state = self.state.lock();
264 state.branch_name = Some(name.to_owned());
265 Ok(())
266 }
267
268 fn create_branch(&self, name: &str) -> Result<()> {
269 let mut state = self.state.lock();
270 state.branch_name = Some(name.to_owned());
271 Ok(())
272 }
273
274 fn blame(&self, path: &Path, _content: Rope) -> Result<crate::blame::Blame> {
275 let state = self.state.lock();
276 state
277 .blames
278 .get(path)
279 .with_context(|| format!("failed to get blame for {:?}", path))
280 .cloned()
281 }
282}
283
284fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
285 match relative_file_path.components().next() {
286 None => anyhow::bail!("repo path should not be empty"),
287 Some(Component::Prefix(_)) => anyhow::bail!(
288 "repo path `{}` should be relative, not a windows prefix",
289 relative_file_path.to_string_lossy()
290 ),
291 Some(Component::RootDir) => {
292 anyhow::bail!(
293 "repo path `{}` should be relative",
294 relative_file_path.to_string_lossy()
295 )
296 }
297 Some(Component::CurDir) => {
298 anyhow::bail!(
299 "repo path `{}` should not start with `.`",
300 relative_file_path.to_string_lossy()
301 )
302 }
303 Some(Component::ParentDir) => {
304 anyhow::bail!(
305 "repo path `{}` should not start with `..`",
306 relative_file_path.to_string_lossy()
307 )
308 }
309 _ => Ok(()),
310 }
311}
312
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
314pub enum GitFileStatus {
315 Added,
316 Modified,
317 Conflict,
318}
319
320impl GitFileStatus {
321 pub fn merge(
322 this: Option<GitFileStatus>,
323 other: Option<GitFileStatus>,
324 prefer_other: bool,
325 ) -> Option<GitFileStatus> {
326 if prefer_other {
327 return other;
328 }
329
330 match (this, other) {
331 (Some(GitFileStatus::Conflict), _) | (_, Some(GitFileStatus::Conflict)) => {
332 Some(GitFileStatus::Conflict)
333 }
334 (Some(GitFileStatus::Modified), _) | (_, Some(GitFileStatus::Modified)) => {
335 Some(GitFileStatus::Modified)
336 }
337 (Some(GitFileStatus::Added), _) | (_, Some(GitFileStatus::Added)) => {
338 Some(GitFileStatus::Added)
339 }
340 _ => None,
341 }
342 }
343}
344
345#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
346pub struct RepoPath(pub PathBuf);
347
348impl RepoPath {
349 pub fn new(path: PathBuf) -> Self {
350 debug_assert!(path.is_relative(), "Repo paths must be relative");
351
352 RepoPath(path)
353 }
354}
355
356impl From<&Path> for RepoPath {
357 fn from(value: &Path) -> Self {
358 RepoPath::new(value.to_path_buf())
359 }
360}
361
362impl From<PathBuf> for RepoPath {
363 fn from(value: PathBuf) -> Self {
364 RepoPath::new(value)
365 }
366}
367
368impl Default for RepoPath {
369 fn default() -> Self {
370 RepoPath(PathBuf::new())
371 }
372}
373
374impl AsRef<Path> for RepoPath {
375 fn as_ref(&self) -> &Path {
376 self.0.as_ref()
377 }
378}
379
380impl std::ops::Deref for RepoPath {
381 type Target = PathBuf;
382
383 fn deref(&self) -> &Self::Target {
384 &self.0
385 }
386}
387
388#[derive(Debug)]
389pub struct RepoPathDescendants<'a>(pub &'a Path);
390
391impl<'a> MapSeekTarget<RepoPath> for RepoPathDescendants<'a> {
392 fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
393 if key.starts_with(self.0) {
394 Ordering::Greater
395 } else {
396 self.0.cmp(key)
397 }
398 }
399}