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

@ -21,11 +21,12 @@ pub struct FakeGitRepository {
pub(crate) fs: Arc<FakeFs>,
pub(crate) executor: BackgroundExecutor,
pub(crate) dot_git_path: PathBuf,
pub(crate) repository_dir_path: PathBuf,
pub(crate) common_dir_path: PathBuf,
}
#[derive(Debug, Clone)]
pub struct FakeGitRepositoryState {
pub path: PathBuf,
pub event_emitter: smol::channel::Sender<PathBuf>,
pub unmerged_paths: HashMap<RepoPath, UnmergedStatus>,
pub head_contents: HashMap<RepoPath, String>,
@ -37,9 +38,8 @@ pub struct FakeGitRepositoryState {
}
impl FakeGitRepositoryState {
pub fn new(path: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
pub fn new(event_emitter: smol::channel::Sender<PathBuf>) -> Self {
FakeGitRepositoryState {
path,
event_emitter,
head_contents: Default::default(),
index_contents: Default::default(),
@ -53,15 +53,6 @@ impl FakeGitRepositoryState {
}
impl FakeGitRepository {
fn with_state<F, T>(&self, f: F) -> T
where
F: FnOnce(&mut FakeGitRepositoryState) -> T,
{
self.fs
.with_git_state(&self.dot_git_path, false, f)
.unwrap()
}
fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
where
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
@ -172,11 +163,11 @@ impl GitRepository for FakeGitRepository {
}
fn path(&self) -> PathBuf {
self.with_state(|state| state.path.clone())
self.repository_dir_path.clone()
}
fn main_repository_path(&self) -> PathBuf {
self.path()
self.common_dir_path.clone()
}
fn merge_message(&self) -> BoxFuture<Option<String>> {
@ -207,8 +198,9 @@ impl GitRepository for FakeGitRepository {
.files()
.iter()
.filter_map(|path| {
// TODO better simulate git status output in the case of submodules and worktrees
let repo_path = path.strip_prefix(workdir_path).ok()?;
let mut is_ignored = false;
let mut is_ignored = repo_path.starts_with(".git");
for ignore in &ignores {
match ignore.matched_path_or_any_parents(path, false) {
ignore::Match::None => {}

View file

@ -851,7 +851,7 @@ impl Watcher for RealWatcher {
pub struct FakeFs {
this: std::sync::Weak<Self>,
// Use an unfair lock to ensure tests are deterministic.
state: Mutex<FakeFsState>,
state: Arc<Mutex<FakeFsState>>,
executor: gpui::BackgroundExecutor,
}
@ -878,6 +878,8 @@ enum FakeFsEntry {
mtime: MTime,
len: u64,
content: Vec<u8>,
// The path to the repository state directory, if this is a gitfile.
git_dir_path: Option<PathBuf>,
},
Dir {
inode: u64,
@ -1036,7 +1038,7 @@ impl FakeFs {
let this = Arc::new_cyclic(|this| Self {
this: this.clone(),
executor: executor.clone(),
state: Mutex::new(FakeFsState {
state: Arc::new(Mutex::new(FakeFsState {
root: Arc::new(Mutex::new(FakeFsEntry::Dir {
inode: 0,
mtime: MTime(UNIX_EPOCH),
@ -1054,7 +1056,7 @@ impl FakeFs {
metadata_call_count: 0,
moves: Default::default(),
home_dir: None,
}),
})),
});
executor.spawn({
@ -1097,6 +1099,7 @@ impl FakeFs {
mtime: new_mtime,
content: Vec::new(),
len: 0,
git_dir_path: None,
})));
}
btree_map::Entry::Occupied(mut e) => match &mut *e.get_mut().lock() {
@ -1154,6 +1157,7 @@ impl FakeFs {
mtime: new_mtime,
len: new_len,
content: new_content,
git_dir_path: None,
})));
}
btree_map::Entry::Occupied(mut e) => {
@ -1278,9 +1282,14 @@ impl FakeFs {
.boxed()
}
pub fn with_git_state<T, F>(&self, dot_git: &Path, emit_git_event: bool, f: F) -> Result<T>
pub fn with_git_state_and_paths<T, F>(
&self,
dot_git: &Path,
emit_git_event: bool,
f: F,
) -> Result<T>
where
F: FnOnce(&mut FakeGitRepositoryState) -> T,
F: FnOnce(&mut FakeGitRepositoryState, &Path, &Path) -> T,
{
let mut state = self.state.lock();
let entry = state.read_path(dot_git).context("open .git")?;
@ -1288,25 +1297,75 @@ impl FakeFs {
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
let repo_state = git_repo_state.get_or_insert_with(|| {
log::debug!("insert git state for {dot_git:?}");
Arc::new(Mutex::new(FakeGitRepositoryState::new(
dot_git.to_path_buf(),
state.git_event_tx.clone(),
)))
});
let mut repo_state = repo_state.lock();
let result = f(&mut repo_state);
let result = f(&mut repo_state, dot_git, dot_git);
if emit_git_event {
state.emit_event([(dot_git, None)]);
}
Ok(result)
} else if let FakeFsEntry::File {
content,
git_dir_path,
..
} = &mut *entry
{
let path = match git_dir_path {
Some(path) => path,
None => {
let path = std::str::from_utf8(content)
.ok()
.and_then(|content| content.strip_prefix("gitdir:"))
.ok_or_else(|| anyhow!("not a valid gitfile"))?
.trim();
git_dir_path.insert(normalize_path(&dot_git.parent().unwrap().join(path)))
}
}
.clone();
drop(entry);
let Some((git_dir_entry, canonical_path)) = state.try_read_path(&path, true) else {
anyhow::bail!("pointed-to git dir {path:?} not found")
};
let FakeFsEntry::Dir { git_repo_state, .. } = &mut *git_dir_entry.lock() else {
anyhow::bail!("gitfile points to a non-directory")
};
let common_dir = canonical_path
.ancestors()
.find(|ancestor| ancestor.ends_with(".git"))
.ok_or_else(|| anyhow!("repository dir not contained in any .git"))?;
let repo_state = git_repo_state.get_or_insert_with(|| {
Arc::new(Mutex::new(FakeGitRepositoryState::new(
state.git_event_tx.clone(),
)))
});
let mut repo_state = repo_state.lock();
let result = f(&mut repo_state, &canonical_path, common_dir);
if emit_git_event {
state.emit_event([(canonical_path, None)]);
}
Ok(result)
} else {
Err(anyhow!("not a directory"))
Err(anyhow!("not a valid git repository"))
}
}
pub fn with_git_state<T, F>(&self, dot_git: &Path, emit_git_event: bool, f: F) -> Result<T>
where
F: FnOnce(&mut FakeGitRepositoryState) -> T,
{
self.with_git_state_and_paths(dot_git, emit_git_event, |state, _, _| f(state))
}
pub fn set_branch_name(&self, dot_git: &Path, branch: Option<impl Into<String>>) {
self.with_git_state(dot_git, true, |state| {
let branch = branch.map(Into::into);
@ -1663,11 +1722,25 @@ impl FakeFsEntry {
}
#[cfg(any(test, feature = "test-support"))]
struct FakeWatcher {}
struct FakeWatcher {
tx: smol::channel::Sender<Vec<PathEvent>>,
original_path: PathBuf,
fs_state: Arc<Mutex<FakeFsState>>,
prefixes: Mutex<Vec<PathBuf>>,
}
#[cfg(any(test, feature = "test-support"))]
impl Watcher for FakeWatcher {
fn add(&self, _: &Path) -> Result<()> {
fn add(&self, path: &Path) -> Result<()> {
if path.starts_with(&self.original_path) {
return Ok(());
}
self.fs_state
.try_lock()
.unwrap()
.event_txs
.push((path.to_owned(), self.tx.clone()));
self.prefixes.lock().push(path.to_owned());
Ok(())
}
@ -1745,6 +1818,7 @@ impl Fs for FakeFs {
mtime,
len: 0,
content: Vec::new(),
git_dir_path: None,
}));
let mut kind = Some(PathEventKind::Created);
state.write_path(path, |entry| {
@ -1901,6 +1975,7 @@ impl Fs for FakeFs {
mtime,
len: content.len() as u64,
content,
git_dir_path: None,
})))
.clone(),
)),
@ -2137,42 +2212,54 @@ impl Fs for FakeFs {
self.simulate_random_delay().await;
let (tx, rx) = smol::channel::unbounded();
let path = path.to_path_buf();
self.state.lock().event_txs.push((path.clone(), tx));
self.state.lock().event_txs.push((path.clone(), tx.clone()));
let executor = self.executor.clone();
let watcher = Arc::new(FakeWatcher {
tx,
original_path: path.to_owned(),
fs_state: self.state.clone(),
prefixes: Mutex::new(vec![path.to_owned()]),
});
(
Box::pin(futures::StreamExt::filter(rx, move |events| {
let result = events
.iter()
.any(|evt_path| evt_path.path.starts_with(&path));
let executor = executor.clone();
async move {
executor.simulate_random_delay().await;
result
Box::pin(futures::StreamExt::filter(rx, {
let watcher = watcher.clone();
move |events| {
let result = events.iter().any(|evt_path| {
let result = watcher
.prefixes
.lock()
.iter()
.any(|prefix| evt_path.path.starts_with(prefix));
result
});
let executor = executor.clone();
async move {
executor.simulate_random_delay().await;
result
}
}
})),
Arc::new(FakeWatcher {}),
watcher,
)
}
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {
let state = self.state.lock();
let entry = state.read_path(abs_dot_git).unwrap();
let mut entry = entry.lock();
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
git_repo_state.get_or_insert_with(|| {
Arc::new(Mutex::new(FakeGitRepositoryState::new(
abs_dot_git.to_path_buf(),
state.git_event_tx.clone(),
)))
});
Some(Arc::new(fake_git_repo::FakeGitRepository {
fs: self.this.upgrade().unwrap(),
executor: self.executor.clone(),
dot_git_path: abs_dot_git.to_path_buf(),
}))
} else {
None
}
use util::ResultExt as _;
self.with_git_state_and_paths(
abs_dot_git,
false,
|_, repository_dir_path, common_dir_path| {
Arc::new(fake_git_repo::FakeGitRepository {
fs: self.this.upgrade().unwrap(),
executor: self.executor.clone(),
dot_git_path: abs_dot_git.to_path_buf(),
repository_dir_path: repository_dir_path.to_owned(),
common_dir_path: common_dir_path.to_owned(),
}) as _
},
)
.log_err()
}
fn git_init(