Directly parse .git when it's a file instead of using libgit2 (#27885)

Avoids building a whole git2 repository object at the worktree layer
just to watch some additional paths.

- [x] Tidy up names of the various paths
- [x] Tests for worktrees and submodules

Release Notes:

- N/A
This commit is contained in:
Cole Miller 2025-04-11 20:35:14 -04:00 committed by GitHub
parent 429d4580cf
commit 055df30757
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 401 additions and 160 deletions

View file

@ -384,12 +384,23 @@ struct LocalRepositoryEntry {
work_directory: WorkDirectory,
work_directory_abs_path: Arc<Path>,
git_dir_scan_id: usize,
original_dot_git_abs_path: Arc<Path>,
/// Absolute path to the actual .git folder.
/// Note: if .git is a file, this points to the folder indicated by the .git file
dot_git_dir_abs_path: Arc<Path>,
/// Absolute path to the .git file, if we're in a git worktree.
dot_git_worktree_abs_path: Option<Arc<Path>>,
/// Absolute path to the original .git entry that caused us to create this repository.
///
/// This is normally a directory, but may be a "gitfile" that points to a directory elsewhere
/// (whose path we then store in `repository_dir_abs_path`).
dot_git_abs_path: Arc<Path>,
/// Absolute path to the "commondir" for this repository.
///
/// This is always a directory. For a normal repository, this is the same as dot_git_abs_path,
/// but in the case of a submodule or a worktree it is the path to the "parent" .git directory
/// from which the submodule/worktree was derived.
common_dir_abs_path: Arc<Path>,
/// Absolute path to the directory holding the repository's state.
///
/// For a normal repository, this is a directory and coincides with `dot_git_abs_path` and
/// `common_dir_abs_path`. For a submodule or worktree, this is some subdirectory of the
/// commondir like `/project/.git/modules/foo`.
repository_dir_abs_path: Arc<Path>,
}
impl sum_tree::Item for LocalRepositoryEntry {
@ -1351,7 +1362,11 @@ impl LocalWorktree {
new_work_directory_abs_path: Some(
new_repo.work_directory_abs_path.clone(),
),
dot_git_abs_path: Some(new_repo.original_dot_git_abs_path.clone()),
dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()),
repository_dir_abs_path: Some(
new_repo.repository_dir_abs_path.clone(),
),
common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()),
});
new_repos.next();
}
@ -1368,9 +1383,11 @@ impl LocalWorktree {
new_work_directory_abs_path: Some(
new_repo.work_directory_abs_path.clone(),
),
dot_git_abs_path: Some(
new_repo.original_dot_git_abs_path.clone(),
dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()),
repository_dir_abs_path: Some(
new_repo.repository_dir_abs_path.clone(),
),
common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()),
});
}
new_repos.next();
@ -1384,6 +1401,8 @@ impl LocalWorktree {
),
new_work_directory_abs_path: None,
dot_git_abs_path: None,
repository_dir_abs_path: None,
common_dir_abs_path: None,
});
old_repos.next();
}
@ -1394,7 +1413,9 @@ impl LocalWorktree {
work_directory_id: entry_id,
old_work_directory_abs_path: None,
new_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
dot_git_abs_path: Some(repo.original_dot_git_abs_path.clone()),
dot_git_abs_path: Some(repo.dot_git_abs_path.clone()),
repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()),
common_dir_abs_path: Some(repo.common_dir_abs_path.clone()),
});
new_repos.next();
}
@ -1403,7 +1424,9 @@ impl LocalWorktree {
work_directory_id: entry_id,
old_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
new_work_directory_abs_path: None,
dot_git_abs_path: Some(repo.original_dot_git_abs_path.clone()),
dot_git_abs_path: Some(repo.dot_git_abs_path.clone()),
repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()),
common_dir_abs_path: Some(repo.common_dir_abs_path.clone()),
});
old_repos.next();
}
@ -3042,9 +3065,6 @@ impl BackgroundScannerState {
);
return;
};
log::debug!(
"building git repository, `.git` path in the worktree: {dot_git_path:?}"
);
parent_dir.into()
}
@ -3075,7 +3095,6 @@ impl BackgroundScannerState {
fs: &dyn Fs,
watcher: &dyn Watcher,
) -> Option<LocalRepositoryEntry> {
log::trace!("insert git repository for {dot_git_path:?}");
let work_dir_entry = self.snapshot.entry_for_path(work_directory.path_key().0)?;
let work_directory_abs_path = self
.snapshot
@ -3092,46 +3111,51 @@ impl BackgroundScannerState {
return None;
}
let dot_git_abs_path = self.snapshot.abs_path.as_path().join(&dot_git_path);
let dot_git_abs_path: Arc<Path> = self
.snapshot
.abs_path
.as_path()
.join(&dot_git_path)
.as_path()
.into();
// TODO add these watchers without building a whole repository by parsing .git-with-indirection
let t0 = Instant::now();
let repository = fs.open_repo(&dot_git_abs_path)?;
log::trace!("opened git repo for {dot_git_abs_path:?}");
let repository_path = repository.path();
watcher.add(&repository_path).log_err()?;
let actual_dot_git_dir_abs_path = repository.main_repository_path();
let dot_git_worktree_abs_path = if actual_dot_git_dir_abs_path == dot_git_abs_path {
None
} else {
// The two paths could be different because we opened a git worktree.
// When that happens:
//
// * `dot_git_abs_path` is a file that points to the worktree-subdirectory in the actual
// .git directory.
//
// * `repository_path` is the worktree-subdirectory.
//
// * `actual_dot_git_dir_abs_path` is the path to the actual .git directory. In git
// documentation this is called the "commondir".
watcher.add(&dot_git_abs_path).log_err()?;
Some(Arc::from(dot_git_abs_path.as_path()))
let mut common_dir_abs_path = dot_git_abs_path.clone();
let mut repository_dir_abs_path = dot_git_abs_path.clone();
// Parse .git if it's a "gitfile" pointing to a repository directory elsewhere.
if let Some(dot_git_contents) = smol::block_on(fs.load(&dot_git_abs_path)).ok() {
if let Some(path) = dot_git_contents.strip_prefix("gitdir:") {
let path = path.trim();
let path = dot_git_abs_path
.parent()
.unwrap_or(Path::new(""))
.join(path);
if let Some(path) = smol::block_on(fs.canonicalize(&path)).log_err() {
repository_dir_abs_path = Path::new(&path).into();
common_dir_abs_path = repository_dir_abs_path.clone();
if let Some(ancestor_dot_git) = path
.ancestors()
.skip(1)
.find(|ancestor| smol::block_on(is_git_dir(ancestor, fs)))
{
common_dir_abs_path = ancestor_dot_git.into();
}
}
} else {
log::error!("failed to parse contents of .git file: {dot_git_contents:?}");
}
};
log::trace!("constructed libgit2 repo in {:?}", t0.elapsed());
watcher.add(&common_dir_abs_path).log_err();
let work_directory_id = work_dir_entry.id;
let local_repository = LocalRepositoryEntry {
work_directory_id,
work_directory,
git_dir_scan_id: 0,
original_dot_git_abs_path: dot_git_abs_path.as_path().into(),
dot_git_dir_abs_path: actual_dot_git_dir_abs_path.into(),
work_directory_abs_path: work_directory_abs_path.as_path().into(),
dot_git_worktree_abs_path,
git_dir_scan_id: 0,
dot_git_abs_path,
common_dir_abs_path,
repository_dir_abs_path,
};
self.snapshot
@ -3454,6 +3478,8 @@ pub struct UpdatedGitRepository {
/// For a normal git repository checkout, the absolute path to the .git directory.
/// For a worktree, the absolute path to the worktree's subdirectory inside the .git directory.
pub dot_git_abs_path: Option<Arc<Path>>,
pub repository_dir_abs_path: Option<Arc<Path>>,
pub common_dir_abs_path: Option<Arc<Path>>,
}
pub type UpdatedEntriesSet = Arc<[(Arc<Path>, ProjectEntryId, PathChange)]>;
@ -4010,8 +4036,8 @@ impl BackgroundScanner {
if abs_path.0.file_name() == Some(*GITIGNORE) {
for (_, repo) in snapshot.git_repositories.iter().filter(|(_, repo)| repo.directory_contains(&relative_path)) {
if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.dot_git_dir_abs_path.as_ref()) {
dot_git_abs_paths.push(repo.dot_git_dir_abs_path.to_path_buf());
if !dot_git_abs_paths.iter().any(|dot_git_abs_path| dot_git_abs_path == repo.common_dir_abs_path.as_ref()) {
dot_git_abs_paths.push(repo.common_dir_abs_path.to_path_buf());
}
}
}
@ -4070,7 +4096,6 @@ impl BackgroundScanner {
}
}
self.send_status_update(false, SmallVec::new());
// send_status_update_inner(phase, state, status_update_tx, false, SmallVec::new());
}
async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> bool {
@ -4709,8 +4734,8 @@ impl BackgroundScanner {
.git_repositories
.iter()
.find_map(|(_, repo)| {
if repo.dot_git_dir_abs_path.as_ref() == &dot_git_dir
|| repo.dot_git_worktree_abs_path.as_deref() == Some(&dot_git_dir)
if repo.common_dir_abs_path.as_ref() == &dot_git_dir
|| repo.repository_dir_abs_path.as_ref() == &dot_git_dir
{
Some(repo.clone())
} else {
@ -4752,7 +4777,7 @@ impl BackgroundScanner {
if exists_in_snapshot
|| matches!(
smol::block_on(self.fs.metadata(&entry.dot_git_dir_abs_path)),
smol::block_on(self.fs.metadata(&entry.common_dir_abs_path)),
Ok(Some(_))
)
{
@ -5081,7 +5106,7 @@ impl WorktreeModelHandle for Entity<Worktree> {
.unwrap();
(
tree.fs.clone(),
local_repo_entry.dot_git_dir_abs_path.clone(),
local_repo_entry.common_dir_abs_path.clone(),
local_repo_entry.git_dir_scan_id,
)
});