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