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