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:
parent
429d4580cf
commit
055df30757
7 changed files with 401 additions and 160 deletions
|
@ -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 => {}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue