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