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