1use crate::status::FileStatus;
2use crate::GitHostingProviderRegistry;
3use crate::{blame::Blame, status::GitStatus};
4use anyhow::{anyhow, Context as _, Result};
5use collections::{HashMap, HashSet};
6use git2::BranchType;
7use gpui::SharedString;
8use parking_lot::Mutex;
9use rope::Rope;
10use std::borrow::Borrow;
11use std::io::Write as _;
12use std::process::Stdio;
13use std::sync::LazyLock;
14use std::{
15 cmp::Ordering,
16 path::{Component, Path, PathBuf},
17 sync::Arc,
18};
19use sum_tree::MapSeekTarget;
20use util::command::new_std_command;
21use util::ResultExt;
22
23#[derive(Clone, Debug, Hash, PartialEq)]
24pub struct Branch {
25 pub is_head: bool,
26 pub name: SharedString,
27 /// Timestamp of most recent commit, normalized to Unix Epoch format.
28 pub unix_timestamp: Option<i64>,
29}
30
31pub trait GitRepository: Send + Sync {
32 fn reload_index(&self);
33
34 /// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
35 ///
36 /// Note that for symlink entries, this will return the contents of the symlink, not the target.
37 fn load_index_text(&self, path: &RepoPath) -> Option<String>;
38
39 /// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path.
40 ///
41 /// Note that for symlink entries, this will return the contents of the symlink, not the target.
42 fn load_committed_text(&self, path: &RepoPath) -> Option<String>;
43
44 fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()>;
45
46 /// Returns the URL of the remote with the given name.
47 fn remote_url(&self, name: &str) -> Option<String>;
48 fn branch_name(&self) -> Option<String>;
49
50 /// Returns the SHA of the current HEAD.
51 fn head_sha(&self) -> Option<String>;
52
53 fn merge_head_shas(&self) -> Vec<String>;
54
55 /// Returns the list of git statuses, sorted by path
56 fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus>;
57
58 fn branches(&self) -> Result<Vec<Branch>>;
59 fn change_branch(&self, _: &str) -> Result<()>;
60 fn create_branch(&self, _: &str) -> Result<()>;
61 fn branch_exits(&self, _: &str) -> Result<bool>;
62
63 fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame>;
64
65 /// Returns the absolute path to the repository. For worktrees, this will be the path to the
66 /// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
67 fn path(&self) -> PathBuf;
68
69 /// Returns the absolute path to the ".git" dir for the main repository, typically a `.git`
70 /// folder. For worktrees, this will be the path to the repository the worktree was created
71 /// from. Otherwise, this is the same value as `path()`.
72 ///
73 /// Git documentation calls this the "commondir", and for git CLI is overridden by
74 /// `GIT_COMMON_DIR`.
75 fn main_repository_path(&self) -> PathBuf;
76
77 /// Updates the index to match the worktree at the given paths.
78 ///
79 /// If any of the paths have been deleted from the worktree, they will be removed from the index if found there.
80 fn stage_paths(&self, paths: &[RepoPath]) -> Result<()>;
81 /// Updates the index to match HEAD at the given paths.
82 ///
83 /// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
84 fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()>;
85
86 fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()>;
87}
88
89impl std::fmt::Debug for dyn GitRepository {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 f.debug_struct("dyn GitRepository<...>").finish()
92 }
93}
94
95pub struct RealGitRepository {
96 pub repository: Mutex<git2::Repository>,
97 pub git_binary_path: PathBuf,
98 hosting_provider_registry: Arc<GitHostingProviderRegistry>,
99}
100
101impl RealGitRepository {
102 pub fn new(
103 repository: git2::Repository,
104 git_binary_path: Option<PathBuf>,
105 hosting_provider_registry: Arc<GitHostingProviderRegistry>,
106 ) -> Self {
107 Self {
108 repository: Mutex::new(repository),
109 git_binary_path: git_binary_path.unwrap_or_else(|| PathBuf::from("git")),
110 hosting_provider_registry,
111 }
112 }
113}
114
115// https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
116const GIT_MODE_SYMLINK: u32 = 0o120000;
117
118impl GitRepository for RealGitRepository {
119 fn reload_index(&self) {
120 if let Ok(mut index) = self.repository.lock().index() {
121 _ = index.read(false);
122 }
123 }
124
125 fn path(&self) -> PathBuf {
126 let repo = self.repository.lock();
127 repo.path().into()
128 }
129
130 fn main_repository_path(&self) -> PathBuf {
131 let repo = self.repository.lock();
132 repo.commondir().into()
133 }
134
135 fn load_index_text(&self, path: &RepoPath) -> Option<String> {
136 fn logic(repo: &git2::Repository, path: &RepoPath) -> Result<Option<String>> {
137 const STAGE_NORMAL: i32 = 0;
138 let index = repo.index()?;
139
140 // This check is required because index.get_path() unwraps internally :(
141 check_path_to_repo_path_errors(path)?;
142
143 let oid = match index.get_path(path, STAGE_NORMAL) {
144 Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
145 _ => return Ok(None),
146 };
147
148 let content = repo.find_blob(oid)?.content().to_owned();
149 Ok(Some(String::from_utf8(content)?))
150 }
151
152 match logic(&self.repository.lock(), path) {
153 Ok(value) => return value,
154 Err(err) => log::error!("Error loading index text: {:?}", err),
155 }
156 None
157 }
158
159 fn load_committed_text(&self, path: &RepoPath) -> Option<String> {
160 let repo = self.repository.lock();
161 let head = repo.head().ok()?.peel_to_tree().log_err()?;
162 let oid = head.get_path(path).ok()?.id();
163 let content = repo.find_blob(oid).log_err()?.content().to_owned();
164 let content = String::from_utf8(content).log_err()?;
165 Some(content)
166 }
167
168 fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
169 let working_directory = self
170 .repository
171 .lock()
172 .workdir()
173 .context("failed to read git work directory")?
174 .to_path_buf();
175 if let Some(content) = content {
176 let mut child = new_std_command(&self.git_binary_path)
177 .current_dir(&working_directory)
178 .args(["hash-object", "-w", "--stdin"])
179 .stdin(Stdio::piped())
180 .stdout(Stdio::piped())
181 .spawn()?;
182 child.stdin.take().unwrap().write_all(content.as_bytes())?;
183 let output = child.wait_with_output()?.stdout;
184 let sha = String::from_utf8(output)?;
185
186 log::debug!("indexing SHA: {sha}, path {path:?}");
187
188 let status = new_std_command(&self.git_binary_path)
189 .current_dir(&working_directory)
190 .args(["update-index", "--add", "--cacheinfo", "100644", &sha])
191 .arg(path.as_ref())
192 .status()?;
193
194 if !status.success() {
195 return Err(anyhow!("Failed to add to index: {status:?}"));
196 }
197 } else {
198 let status = new_std_command(&self.git_binary_path)
199 .current_dir(&working_directory)
200 .args(["update-index", "--force-remove"])
201 .arg(path.as_ref())
202 .status()?;
203
204 if !status.success() {
205 return Err(anyhow!("Failed to remove from index: {status:?}"));
206 }
207 }
208
209 Ok(())
210 }
211
212 fn remote_url(&self, name: &str) -> Option<String> {
213 let repo = self.repository.lock();
214 let remote = repo.find_remote(name).ok()?;
215 remote.url().map(|url| url.to_string())
216 }
217
218 fn branch_name(&self) -> Option<String> {
219 let repo = self.repository.lock();
220 let head = repo.head().log_err()?;
221 let branch = String::from_utf8_lossy(head.shorthand_bytes());
222 Some(branch.to_string())
223 }
224
225 fn head_sha(&self) -> Option<String> {
226 Some(self.repository.lock().head().ok()?.target()?.to_string())
227 }
228
229 fn merge_head_shas(&self) -> Vec<String> {
230 let mut shas = Vec::default();
231 self.repository
232 .lock()
233 .mergehead_foreach(|oid| {
234 shas.push(oid.to_string());
235 true
236 })
237 .ok();
238 shas
239 }
240
241 fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
242 let working_directory = self
243 .repository
244 .lock()
245 .workdir()
246 .context("failed to read git work directory")?
247 .to_path_buf();
248 GitStatus::new(&self.git_binary_path, &working_directory, path_prefixes)
249 }
250
251 fn branch_exits(&self, name: &str) -> Result<bool> {
252 let repo = self.repository.lock();
253 let branch = repo.find_branch(name, BranchType::Local);
254 match branch {
255 Ok(_) => Ok(true),
256 Err(e) => match e.code() {
257 git2::ErrorCode::NotFound => Ok(false),
258 _ => Err(anyhow!(e)),
259 },
260 }
261 }
262
263 fn branches(&self) -> Result<Vec<Branch>> {
264 let repo = self.repository.lock();
265 let local_branches = repo.branches(Some(BranchType::Local))?;
266 let valid_branches = local_branches
267 .filter_map(|branch| {
268 branch.ok().and_then(|(branch, _)| {
269 let is_head = branch.is_head();
270 let name = branch
271 .name()
272 .ok()
273 .flatten()
274 .map(|name| name.to_string().into())?;
275 let timestamp = branch.get().peel_to_commit().ok()?.time();
276 let unix_timestamp = timestamp.seconds();
277 let timezone_offset = timestamp.offset_minutes();
278 let utc_offset =
279 time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
280 let unix_timestamp =
281 time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
282 Some(Branch {
283 is_head,
284 name,
285 unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
286 })
287 })
288 })
289 .collect();
290 Ok(valid_branches)
291 }
292
293 fn change_branch(&self, name: &str) -> Result<()> {
294 let repo = self.repository.lock();
295 let revision = repo.find_branch(name, BranchType::Local)?;
296 let revision = revision.get();
297 let as_tree = revision.peel_to_tree()?;
298 repo.checkout_tree(as_tree.as_object(), None)?;
299 repo.set_head(
300 revision
301 .name()
302 .ok_or_else(|| anyhow!("Branch name could not be retrieved"))?,
303 )?;
304 Ok(())
305 }
306
307 fn create_branch(&self, name: &str) -> Result<()> {
308 let repo = self.repository.lock();
309 let current_commit = repo.head()?.peel_to_commit()?;
310 repo.branch(name, ¤t_commit, false)?;
311 Ok(())
312 }
313
314 fn blame(&self, path: &Path, content: Rope) -> Result<crate::blame::Blame> {
315 let working_directory = self
316 .repository
317 .lock()
318 .workdir()
319 .with_context(|| format!("failed to get git working directory for file {:?}", path))?
320 .to_path_buf();
321
322 const REMOTE_NAME: &str = "origin";
323 let remote_url = self.remote_url(REMOTE_NAME);
324
325 crate::blame::Blame::for_path(
326 &self.git_binary_path,
327 &working_directory,
328 path,
329 &content,
330 remote_url,
331 self.hosting_provider_registry.clone(),
332 )
333 }
334
335 fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
336 let working_directory = self
337 .repository
338 .lock()
339 .workdir()
340 .context("failed to read git work directory")?
341 .to_path_buf();
342
343 if !paths.is_empty() {
344 let output = new_std_command(&self.git_binary_path)
345 .current_dir(&working_directory)
346 .args(["update-index", "--add", "--remove", "--"])
347 .args(paths.iter().map(|p| p.as_ref()))
348 .output()?;
349 if !output.status.success() {
350 return Err(anyhow!(
351 "Failed to stage paths:\n{}",
352 String::from_utf8_lossy(&output.stderr)
353 ));
354 }
355 }
356 Ok(())
357 }
358
359 fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
360 let working_directory = self
361 .repository
362 .lock()
363 .workdir()
364 .context("failed to read git work directory")?
365 .to_path_buf();
366
367 if !paths.is_empty() {
368 let output = new_std_command(&self.git_binary_path)
369 .current_dir(&working_directory)
370 .args(["reset", "--quiet", "--"])
371 .args(paths.iter().map(|p| p.as_ref()))
372 .output()?;
373 if !output.status.success() {
374 return Err(anyhow!(
375 "Failed to unstage:\n{}",
376 String::from_utf8_lossy(&output.stderr)
377 ));
378 }
379 }
380 Ok(())
381 }
382
383 fn commit(&self, message: &str, name_and_email: Option<(&str, &str)>) -> Result<()> {
384 let working_directory = self
385 .repository
386 .lock()
387 .workdir()
388 .context("failed to read git work directory")?
389 .to_path_buf();
390 let mut args = vec!["commit", "--quiet", "-m", message, "--cleanup=strip"];
391 let author = name_and_email.map(|(name, email)| format!("{name} <{email}>"));
392 if let Some(author) = author.as_deref() {
393 args.push("--author");
394 args.push(author);
395 }
396
397 let output = new_std_command(&self.git_binary_path)
398 .current_dir(&working_directory)
399 .args(args)
400 .output()?;
401
402 if !output.status.success() {
403 return Err(anyhow!(
404 "Failed to commit:\n{}",
405 String::from_utf8_lossy(&output.stderr)
406 ));
407 }
408 Ok(())
409 }
410}
411
412#[derive(Debug, Clone)]
413pub struct FakeGitRepository {
414 state: Arc<Mutex<FakeGitRepositoryState>>,
415}
416
417#[derive(Debug, Clone)]
418pub struct FakeGitRepositoryState {
419 pub path: PathBuf,
420 pub event_emitter: smol::channel::Sender<PathBuf>,
421 pub head_contents: HashMap<RepoPath, String>,
422 pub index_contents: HashMap<RepoPath, String>,
423 pub blames: HashMap<RepoPath, Blame>,
424 pub statuses: HashMap<RepoPath, FileStatus>,
425 pub current_branch_name: Option<String>,
426 pub branches: HashSet<String>,
427}
428
429impl FakeGitRepository {
430 pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
431 Arc::new(FakeGitRepository { state })
432 }
433}
434
435impl FakeGitRepositoryState {
436 pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
437 FakeGitRepositoryState {
438 path,
439 event_emitter,
440 head_contents: Default::default(),
441 index_contents: Default::default(),
442 blames: Default::default(),
443 statuses: Default::default(),
444 current_branch_name: Default::default(),
445 branches: Default::default(),
446 }
447 }
448}
449
450impl GitRepository for FakeGitRepository {
451 fn reload_index(&self) {}
452
453 fn load_index_text(&self, path: &RepoPath) -> Option<String> {
454 let state = self.state.lock();
455 state.index_contents.get(path.as_ref()).cloned()
456 }
457
458 fn load_committed_text(&self, path: &RepoPath) -> Option<String> {
459 let state = self.state.lock();
460 state.head_contents.get(path.as_ref()).cloned()
461 }
462
463 fn set_index_text(&self, path: &RepoPath, content: Option<String>) -> anyhow::Result<()> {
464 let mut state = self.state.lock();
465 if let Some(content) = content {
466 state.index_contents.insert(path.clone(), content);
467 } else {
468 state.index_contents.remove(path);
469 }
470 state
471 .event_emitter
472 .try_send(state.path.clone())
473 .expect("Dropped repo change event");
474 Ok(())
475 }
476
477 fn remote_url(&self, _name: &str) -> Option<String> {
478 None
479 }
480
481 fn branch_name(&self) -> Option<String> {
482 let state = self.state.lock();
483 state.current_branch_name.clone()
484 }
485
486 fn head_sha(&self) -> Option<String> {
487 None
488 }
489
490 fn merge_head_shas(&self) -> Vec<String> {
491 vec![]
492 }
493
494 fn path(&self) -> PathBuf {
495 let state = self.state.lock();
496 state.path.clone()
497 }
498
499 fn main_repository_path(&self) -> PathBuf {
500 self.path()
501 }
502
503 fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
504 let state = self.state.lock();
505
506 let mut entries = state
507 .statuses
508 .iter()
509 .filter_map(|(repo_path, status)| {
510 if path_prefixes
511 .iter()
512 .any(|path_prefix| repo_path.0.starts_with(path_prefix))
513 {
514 Some((repo_path.to_owned(), *status))
515 } else {
516 None
517 }
518 })
519 .collect::<Vec<_>>();
520 entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
521
522 Ok(GitStatus {
523 entries: entries.into(),
524 })
525 }
526
527 fn branches(&self) -> Result<Vec<Branch>> {
528 let state = self.state.lock();
529 let current_branch = &state.current_branch_name;
530 Ok(state
531 .branches
532 .iter()
533 .map(|branch_name| Branch {
534 is_head: Some(branch_name) == current_branch.as_ref(),
535 name: branch_name.into(),
536 unix_timestamp: None,
537 })
538 .collect())
539 }
540
541 fn branch_exits(&self, name: &str) -> Result<bool> {
542 let state = self.state.lock();
543 Ok(state.branches.contains(name))
544 }
545
546 fn change_branch(&self, name: &str) -> Result<()> {
547 let mut state = self.state.lock();
548 state.current_branch_name = Some(name.to_owned());
549 state
550 .event_emitter
551 .try_send(state.path.clone())
552 .expect("Dropped repo change event");
553 Ok(())
554 }
555
556 fn create_branch(&self, name: &str) -> Result<()> {
557 let mut state = self.state.lock();
558 state.branches.insert(name.to_owned());
559 state
560 .event_emitter
561 .try_send(state.path.clone())
562 .expect("Dropped repo change event");
563 Ok(())
564 }
565
566 fn blame(&self, path: &Path, _content: Rope) -> Result<crate::blame::Blame> {
567 let state = self.state.lock();
568 state
569 .blames
570 .get(path)
571 .with_context(|| format!("failed to get blame for {:?}", path))
572 .cloned()
573 }
574
575 fn stage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
576 unimplemented!()
577 }
578
579 fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
580 unimplemented!()
581 }
582
583 fn commit(&self, _message: &str, _name_and_email: Option<(&str, &str)>) -> Result<()> {
584 unimplemented!()
585 }
586}
587
588fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> {
589 match relative_file_path.components().next() {
590 None => anyhow::bail!("repo path should not be empty"),
591 Some(Component::Prefix(_)) => anyhow::bail!(
592 "repo path `{}` should be relative, not a windows prefix",
593 relative_file_path.to_string_lossy()
594 ),
595 Some(Component::RootDir) => {
596 anyhow::bail!(
597 "repo path `{}` should be relative",
598 relative_file_path.to_string_lossy()
599 )
600 }
601 Some(Component::CurDir) => {
602 anyhow::bail!(
603 "repo path `{}` should not start with `.`",
604 relative_file_path.to_string_lossy()
605 )
606 }
607 Some(Component::ParentDir) => {
608 anyhow::bail!(
609 "repo path `{}` should not start with `..`",
610 relative_file_path.to_string_lossy()
611 )
612 }
613 _ => Ok(()),
614 }
615}
616
617pub static WORK_DIRECTORY_REPO_PATH: LazyLock<RepoPath> =
618 LazyLock::new(|| RepoPath(Path::new("").into()));
619
620#[derive(Clone, Debug, Ord, Hash, PartialOrd, Eq, PartialEq)]
621pub struct RepoPath(pub Arc<Path>);
622
623impl RepoPath {
624 pub fn new(path: PathBuf) -> Self {
625 debug_assert!(path.is_relative(), "Repo paths must be relative");
626
627 RepoPath(path.into())
628 }
629
630 pub fn from_str(path: &str) -> Self {
631 let path = Path::new(path);
632 debug_assert!(path.is_relative(), "Repo paths must be relative");
633
634 RepoPath(path.into())
635 }
636}
637
638impl std::fmt::Display for RepoPath {
639 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640 self.0.to_string_lossy().fmt(f)
641 }
642}
643
644impl From<&Path> for RepoPath {
645 fn from(value: &Path) -> Self {
646 RepoPath::new(value.into())
647 }
648}
649
650impl From<Arc<Path>> for RepoPath {
651 fn from(value: Arc<Path>) -> Self {
652 RepoPath(value)
653 }
654}
655
656impl From<PathBuf> for RepoPath {
657 fn from(value: PathBuf) -> Self {
658 RepoPath::new(value)
659 }
660}
661
662impl From<&str> for RepoPath {
663 fn from(value: &str) -> Self {
664 Self::from_str(value)
665 }
666}
667
668impl Default for RepoPath {
669 fn default() -> Self {
670 RepoPath(Path::new("").into())
671 }
672}
673
674impl AsRef<Path> for RepoPath {
675 fn as_ref(&self) -> &Path {
676 self.0.as_ref()
677 }
678}
679
680impl std::ops::Deref for RepoPath {
681 type Target = Path;
682
683 fn deref(&self) -> &Self::Target {
684 &self.0
685 }
686}
687
688impl Borrow<Path> for RepoPath {
689 fn borrow(&self) -> &Path {
690 self.0.as_ref()
691 }
692}
693
694#[derive(Debug)]
695pub struct RepoPathDescendants<'a>(pub &'a Path);
696
697impl<'a> MapSeekTarget<RepoPath> for RepoPathDescendants<'a> {
698 fn cmp_cursor(&self, key: &RepoPath) -> Ordering {
699 if key.starts_with(self.0) {
700 Ordering::Greater
701 } else {
702 self.0.cmp(key)
703 }
704 }
705}