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