Compare commits
6 Commits
arm_github
...
cole/git-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4d9de293f9 | ||
|
|
caade6af7a | ||
|
|
984eb9b5c3 | ||
|
|
00dea4ccc9 | ||
|
|
abb295ea22 | ||
|
|
33fa0724cf |
@@ -5,9 +5,9 @@ mod mac_watcher;
|
|||||||
pub mod fs_watcher;
|
pub mod fs_watcher;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
use git::status::FileStatus;
|
|
||||||
use git::GitHostingProviderRegistry;
|
use git::GitHostingProviderRegistry;
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
use git::{repository::RepoPath, status::FileStatus};
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
use ashpd::desktop::trash;
|
use ashpd::desktop::trash;
|
||||||
@@ -892,58 +892,7 @@ impl FakeFsState {
|
|||||||
target: &Path,
|
target: &Path,
|
||||||
follow_symlink: bool,
|
follow_symlink: bool,
|
||||||
) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
|
) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
|
||||||
let mut path = target.to_path_buf();
|
FakeFsEntry::try_read_path(&self.root, target, follow_symlink)
|
||||||
let mut canonical_path = PathBuf::new();
|
|
||||||
let mut entry_stack = Vec::new();
|
|
||||||
'outer: loop {
|
|
||||||
let mut path_components = path.components().peekable();
|
|
||||||
let mut prefix = None;
|
|
||||||
while let Some(component) = path_components.next() {
|
|
||||||
match component {
|
|
||||||
Component::Prefix(prefix_component) => prefix = Some(prefix_component),
|
|
||||||
Component::RootDir => {
|
|
||||||
entry_stack.clear();
|
|
||||||
entry_stack.push(self.root.clone());
|
|
||||||
canonical_path.clear();
|
|
||||||
match prefix {
|
|
||||||
Some(prefix_component) => {
|
|
||||||
canonical_path = PathBuf::from(prefix_component.as_os_str());
|
|
||||||
// Prefixes like `C:\\` are represented without their trailing slash, so we have to re-add it.
|
|
||||||
canonical_path.push(std::path::MAIN_SEPARATOR_STR);
|
|
||||||
}
|
|
||||||
None => canonical_path = PathBuf::from(std::path::MAIN_SEPARATOR_STR),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component::CurDir => {}
|
|
||||||
Component::ParentDir => {
|
|
||||||
entry_stack.pop()?;
|
|
||||||
canonical_path.pop();
|
|
||||||
}
|
|
||||||
Component::Normal(name) => {
|
|
||||||
let current_entry = entry_stack.last().cloned()?;
|
|
||||||
let current_entry = current_entry.lock();
|
|
||||||
if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
|
|
||||||
let entry = entries.get(name.to_str().unwrap()).cloned()?;
|
|
||||||
if path_components.peek().is_some() || follow_symlink {
|
|
||||||
let entry = entry.lock();
|
|
||||||
if let FakeFsEntry::Symlink { target, .. } = &*entry {
|
|
||||||
let mut target = target.clone();
|
|
||||||
target.extend(path_components);
|
|
||||||
path = target;
|
|
||||||
continue 'outer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entry_stack.push(entry.clone());
|
|
||||||
canonical_path = canonical_path.join(name);
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Some((entry_stack.pop()?, canonical_path))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
|
fn write_path<Fn, T>(&self, path: &Path, callback: Fn) -> Result<T>
|
||||||
@@ -1227,16 +1176,23 @@ impl FakeFs {
|
|||||||
F: FnOnce(&mut FakeGitRepositoryState),
|
F: FnOnce(&mut FakeGitRepositoryState),
|
||||||
{
|
{
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
let entry = state.read_path(dot_git).unwrap();
|
let dot_git_entry = state.read_path(dot_git).unwrap();
|
||||||
let mut entry = entry.lock();
|
let mut dot_git_entry = dot_git_entry.lock();
|
||||||
|
let parent = dot_git.parent().expect(".git has no parent path");
|
||||||
|
let parent_entry = state.read_path(parent).unwrap();
|
||||||
|
|
||||||
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *dot_git_entry {
|
||||||
let repo_state = git_repo_state.get_or_insert_with(|| {
|
let git_fs = FakeGitRepositoryFs {
|
||||||
Arc::new(Mutex::new(FakeGitRepositoryState::new(
|
dot_git_dir: dot_git.to_owned(),
|
||||||
dot_git.to_path_buf(),
|
entry: parent_entry,
|
||||||
state.git_event_tx.clone(),
|
event_emitter: state.git_event_tx.clone(),
|
||||||
)))
|
};
|
||||||
});
|
let repo_state = git_repo_state
|
||||||
|
.get_or_insert_with(|| {
|
||||||
|
Arc::new(Mutex::new(FakeGitRepositoryState::new(Arc::new(git_fs))))
|
||||||
|
})
|
||||||
|
.clone();
|
||||||
|
drop(dot_git_entry);
|
||||||
let mut repo_state = repo_state.lock();
|
let mut repo_state = repo_state.lock();
|
||||||
|
|
||||||
f(&mut repo_state);
|
f(&mut repo_state);
|
||||||
@@ -1276,7 +1232,7 @@ impl FakeFs {
|
|||||||
state.index_contents.extend(
|
state.index_contents.extend(
|
||||||
head_state
|
head_state
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(path, content)| (path.to_path_buf(), content.clone())),
|
.map(|(path, content)| ((*path).into(), content.clone())),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1284,11 +1240,9 @@ impl FakeFs {
|
|||||||
pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(&Path, git::blame::Blame)>) {
|
pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(&Path, git::blame::Blame)>) {
|
||||||
self.with_git_state(dot_git, true, |state| {
|
self.with_git_state(dot_git, true, |state| {
|
||||||
state.blames.clear();
|
state.blames.clear();
|
||||||
state.blames.extend(
|
state
|
||||||
blames
|
.blames
|
||||||
.into_iter()
|
.extend(blames.into_iter().map(|(path, blame)| (path.into(), blame)));
|
||||||
.map(|(path, blame)| (path.to_path_buf(), blame)),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1431,6 +1385,65 @@ impl FakeFsEntry {
|
|||||||
Err(anyhow!("not a directory: {}", path.display()))
|
Err(anyhow!("not a directory: {}", path.display()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn try_read_path(
|
||||||
|
this: &Arc<Mutex<Self>>,
|
||||||
|
target: &Path,
|
||||||
|
follow_symlink: bool,
|
||||||
|
) -> Option<(Arc<Mutex<FakeFsEntry>>, PathBuf)> {
|
||||||
|
let mut path = target.to_path_buf();
|
||||||
|
let mut canonical_path = PathBuf::new();
|
||||||
|
let mut entry_stack = Vec::new();
|
||||||
|
'outer: loop {
|
||||||
|
let mut path_components = path.components().peekable();
|
||||||
|
let mut prefix = None;
|
||||||
|
while let Some(component) = path_components.next() {
|
||||||
|
match component {
|
||||||
|
Component::Prefix(prefix_component) => prefix = Some(prefix_component),
|
||||||
|
Component::RootDir => {
|
||||||
|
entry_stack.clear();
|
||||||
|
entry_stack.push(this.clone());
|
||||||
|
canonical_path.clear();
|
||||||
|
match prefix {
|
||||||
|
Some(prefix_component) => {
|
||||||
|
canonical_path = PathBuf::from(prefix_component.as_os_str());
|
||||||
|
// Prefixes like `C:\\` are represented without their trailing slash, so we have to re-add it.
|
||||||
|
canonical_path.push(std::path::MAIN_SEPARATOR_STR);
|
||||||
|
}
|
||||||
|
None => canonical_path = PathBuf::from(std::path::MAIN_SEPARATOR_STR),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component::CurDir => {}
|
||||||
|
Component::ParentDir => {
|
||||||
|
entry_stack.pop()?;
|
||||||
|
canonical_path.pop();
|
||||||
|
}
|
||||||
|
Component::Normal(name) => {
|
||||||
|
let current_entry = entry_stack.last().cloned()?;
|
||||||
|
let current_entry = current_entry.lock();
|
||||||
|
if let FakeFsEntry::Dir { entries, .. } = &*current_entry {
|
||||||
|
let entry = entries.get(name.to_str().unwrap()).cloned()?;
|
||||||
|
if path_components.peek().is_some() || follow_symlink {
|
||||||
|
let entry = entry.lock();
|
||||||
|
if let FakeFsEntry::Symlink { target, .. } = &*entry {
|
||||||
|
let mut target = target.clone();
|
||||||
|
target.extend(path_components);
|
||||||
|
path = target;
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entry_stack.push(entry.clone());
|
||||||
|
canonical_path = canonical_path.join(name);
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Some((entry_stack.pop()?, canonical_path))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
@@ -1927,15 +1940,20 @@ impl Fs for FakeFs {
|
|||||||
|
|
||||||
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {
|
fn open_repo(&self, abs_dot_git: &Path) -> Option<Arc<dyn GitRepository>> {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
let entry = state.read_path(abs_dot_git).unwrap();
|
let dot_git_entry = state.read_path(abs_dot_git).unwrap();
|
||||||
let mut entry = entry.lock();
|
let mut dot_git_entry = dot_git_entry.lock();
|
||||||
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
let parent = abs_dot_git.parent().expect(".git has no parent path");
|
||||||
|
let parent_entry = state.read_path(parent).unwrap();
|
||||||
|
|
||||||
|
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *dot_git_entry {
|
||||||
|
let git_fs = FakeGitRepositoryFs {
|
||||||
|
dot_git_dir: abs_dot_git.to_owned(),
|
||||||
|
entry: parent_entry,
|
||||||
|
event_emitter: state.git_event_tx.clone(),
|
||||||
|
};
|
||||||
let state = git_repo_state
|
let state = git_repo_state
|
||||||
.get_or_insert_with(|| {
|
.get_or_insert_with(|| {
|
||||||
Arc::new(Mutex::new(FakeGitRepositoryState::new(
|
Arc::new(Mutex::new(FakeGitRepositoryState::new(Arc::new(git_fs))))
|
||||||
abs_dot_git.to_path_buf(),
|
|
||||||
state.git_event_tx.clone(),
|
|
||||||
)))
|
|
||||||
})
|
})
|
||||||
.clone();
|
.clone();
|
||||||
Some(git::repository::FakeGitRepository::open(state))
|
Some(git::repository::FakeGitRepository::open(state))
|
||||||
@@ -1958,6 +1976,77 @@ impl Fs for FakeFs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
struct FakeGitRepositoryFs {
|
||||||
|
dot_git_dir: PathBuf,
|
||||||
|
entry: Arc<Mutex<FakeFsEntry>>,
|
||||||
|
event_emitter: smol::channel::Sender<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl git::repository::FakeGitRepositoryFs for FakeGitRepositoryFs {
|
||||||
|
fn dot_git_dir(&self) -> PathBuf {
|
||||||
|
self.dot_git_dir.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_path(&self, path: &RepoPath) -> Option<String> {
|
||||||
|
let (entry, _) = FakeFsEntry::try_read_path(&self.entry, path, false)?;
|
||||||
|
let content = entry.lock().file_content(path).ok()?.clone();
|
||||||
|
let content = String::from_utf8(content).expect("Non-UTF-8 content in FakeFs entry");
|
||||||
|
Some(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn all_paths(&self) -> Box<dyn Iterator<Item = (RepoPath, String)>> {
|
||||||
|
let mut stack = vec![];
|
||||||
|
let mut start = "".to_owned();
|
||||||
|
let mut entry = self.entry.clone();
|
||||||
|
let mut path: PathBuf = "".into();
|
||||||
|
let mut result = Vec::new();
|
||||||
|
'outer: loop {
|
||||||
|
let cur = entry.clone();
|
||||||
|
for (segment, child) in cur.lock().dir_entries(&path).unwrap().range(start..) {
|
||||||
|
let guard = child.lock();
|
||||||
|
match &*guard {
|
||||||
|
FakeFsEntry::File { content, .. } => {
|
||||||
|
let content = String::from_utf8(content.clone())
|
||||||
|
.expect("Non-UTF-8 content in FakeFs entry");
|
||||||
|
result.push((path.join(segment).into(), content));
|
||||||
|
}
|
||||||
|
FakeFsEntry::Dir {
|
||||||
|
git_repo_state,
|
||||||
|
entries,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if git_repo_state.is_some()
|
||||||
|
|| entries.keys().any(|segment| segment == ".git")
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stack.push((entry.clone(), segment.clone()));
|
||||||
|
entry = child.clone();
|
||||||
|
start = "".to_owned();
|
||||||
|
path = path.join(segment);
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
FakeFsEntry::Symlink { .. } => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some((parent, parent_start)) = stack.pop() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
entry = parent;
|
||||||
|
start = parent_start;
|
||||||
|
path.pop();
|
||||||
|
}
|
||||||
|
Box::new(result.into_iter())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn repo_changed(&self) {
|
||||||
|
self.event_emitter
|
||||||
|
.try_send(self.dot_git_dir.clone())
|
||||||
|
.expect("Dropped repo change notification")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
|
fn chunks(rope: &Rope, line_ending: LineEnding) -> impl Iterator<Item = &str> {
|
||||||
rope.chunks().flat_map(move |chunk| {
|
rope.chunks().flat_map(move |chunk| {
|
||||||
let mut newline = false;
|
let mut newline = false;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::status::FileStatus;
|
use crate::status::{FileStatus, StatusCode, TrackedStatus};
|
||||||
use crate::GitHostingProviderRegistry;
|
use crate::GitHostingProviderRegistry;
|
||||||
use crate::{blame::Blame, status::GitStatus};
|
use crate::{blame::Blame, status::GitStatus};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
@@ -31,7 +31,7 @@ pub trait GitRepository: Send + Sync {
|
|||||||
|
|
||||||
/// Loads a git repository entry's contents.
|
/// Loads a git repository entry's contents.
|
||||||
/// Note that for symlink entries, this will return the contents of the symlink, not the target.
|
/// Note that for symlink entries, this will return the contents of the symlink, not the target.
|
||||||
fn load_index_text(&self, relative_file_path: &Path) -> Option<String>;
|
fn load_index_text(&self, relative_file_path: &RepoPath) -> Option<String>;
|
||||||
|
|
||||||
/// Returns the URL of the remote with the given name.
|
/// Returns the URL of the remote with the given name.
|
||||||
fn remote_url(&self, name: &str) -> Option<String>;
|
fn remote_url(&self, name: &str) -> Option<String>;
|
||||||
@@ -106,7 +106,7 @@ impl GitRepository for RealGitRepository {
|
|||||||
repo.path().into()
|
repo.path().into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_index_text(&self, relative_file_path: &Path) -> Option<String> {
|
fn load_index_text(&self, relative_file_path: &RepoPath) -> Option<String> {
|
||||||
fn logic(repo: &git2::Repository, relative_file_path: &Path) -> Result<Option<String>> {
|
fn logic(repo: &git2::Repository, relative_file_path: &Path) -> Result<Option<String>> {
|
||||||
const STAGE_NORMAL: i32 = 0;
|
const STAGE_NORMAL: i32 = 0;
|
||||||
let index = repo.index()?;
|
let index = repo.index()?;
|
||||||
@@ -307,17 +307,38 @@ pub struct FakeGitRepository {
|
|||||||
state: Arc<Mutex<FakeGitRepositoryState>>,
|
state: Arc<Mutex<FakeGitRepositoryState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
/// Stub for the Fs trait to break what would otherwise be a circular dependency between fs and git.
|
||||||
|
pub trait FakeGitRepositoryFs: Send + Sync {
|
||||||
|
fn dot_git_dir(&self) -> PathBuf;
|
||||||
|
fn read_path(&self, path: &RepoPath) -> Option<String>;
|
||||||
|
fn all_paths(&self) -> Box<dyn Iterator<Item = (RepoPath, String)>>;
|
||||||
|
fn repo_changed(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct FakeGitRepositoryState {
|
pub struct FakeGitRepositoryState {
|
||||||
pub dot_git_dir: PathBuf,
|
pub fake_fs: Arc<dyn FakeGitRepositoryFs>,
|
||||||
pub event_emitter: smol::channel::Sender<PathBuf>,
|
pub head_contents: HashMap<RepoPath, String>,
|
||||||
pub index_contents: HashMap<PathBuf, String>,
|
pub index_contents: HashMap<RepoPath, String>,
|
||||||
pub blames: HashMap<PathBuf, Blame>,
|
pub blames: HashMap<RepoPath, Blame>,
|
||||||
pub statuses: HashMap<RepoPath, FileStatus>,
|
pub statuses: HashMap<RepoPath, FileStatus>,
|
||||||
pub current_branch_name: Option<String>,
|
pub current_branch_name: Option<String>,
|
||||||
pub branches: HashSet<String>,
|
pub branches: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for FakeGitRepositoryState {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("FakeGitRepositoryState")
|
||||||
|
.field("head_contents", &self.head_contents)
|
||||||
|
.field("index_contents", &self.index_contents)
|
||||||
|
.field("blames", &self.blames)
|
||||||
|
.field("statuses", &self.statuses)
|
||||||
|
.field("current_branch_name", &self.current_branch_name)
|
||||||
|
.field("branches", &self.branches)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FakeGitRepository {
|
impl FakeGitRepository {
|
||||||
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
|
pub fn open(state: Arc<Mutex<FakeGitRepositoryState>>) -> Arc<dyn GitRepository> {
|
||||||
Arc::new(FakeGitRepository { state })
|
Arc::new(FakeGitRepository { state })
|
||||||
@@ -325,10 +346,10 @@ impl FakeGitRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FakeGitRepositoryState {
|
impl FakeGitRepositoryState {
|
||||||
pub fn new(dot_git_dir: PathBuf, event_emitter: smol::channel::Sender<PathBuf>) -> Self {
|
pub fn new(fake_fs: Arc<dyn FakeGitRepositoryFs>) -> Self {
|
||||||
FakeGitRepositoryState {
|
FakeGitRepositoryState {
|
||||||
dot_git_dir,
|
fake_fs,
|
||||||
event_emitter,
|
head_contents: Default::default(),
|
||||||
index_contents: Default::default(),
|
index_contents: Default::default(),
|
||||||
blames: Default::default(),
|
blames: Default::default(),
|
||||||
statuses: Default::default(),
|
statuses: Default::default(),
|
||||||
@@ -336,12 +357,79 @@ impl FakeGitRepositoryState {
|
|||||||
branches: Default::default(),
|
branches: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_statuses(&mut self) {
|
||||||
|
let mut paths = self
|
||||||
|
.head_contents
|
||||||
|
.iter()
|
||||||
|
.map(|(path, contents)| (path.clone(), Some(contents.clone()), None, None))
|
||||||
|
.chain(
|
||||||
|
self.index_contents
|
||||||
|
.iter()
|
||||||
|
.map(|(path, contents)| (path.clone(), None, Some(contents.clone()), None)),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
self.fake_fs
|
||||||
|
.all_paths()
|
||||||
|
.map(|(path, contents)| (path, None, None, Some(contents))),
|
||||||
|
)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
paths.sort_by(|(a, _, _, _), (b, _, _, _)| a.cmp(&b));
|
||||||
|
paths.dedup_by(
|
||||||
|
|(a, head_a, index_a, worktree_a), (b, head_b, index_b, worktree_b)| {
|
||||||
|
if a != b {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*head_b = head_b.take().or(head_a.take());
|
||||||
|
*index_b = index_b.take().or(index_a.take());
|
||||||
|
*worktree_b = worktree_b.take().or(worktree_a.take());
|
||||||
|
true
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.statuses.clear();
|
||||||
|
self.statuses.extend(
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(path, head, index, worktree)| {
|
||||||
|
fn status_code(a: &Option<String>, b: &Option<String>) -> StatusCode {
|
||||||
|
match (a, b) {
|
||||||
|
(None, None) => StatusCode::Unmodified,
|
||||||
|
(Some(a), Some(b)) => {
|
||||||
|
if a == b {
|
||||||
|
StatusCode::Modified
|
||||||
|
} else {
|
||||||
|
StatusCode::Unmodified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, Some(_)) => StatusCode::Added,
|
||||||
|
(Some(_), None) => StatusCode::Deleted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = match (head, index, worktree) {
|
||||||
|
(None, None, None) => return None,
|
||||||
|
(Some(head), Some(index), Some(worktree))
|
||||||
|
if head == index && index == worktree =>
|
||||||
|
{
|
||||||
|
return None
|
||||||
|
}
|
||||||
|
(None, None, Some(_)) => FileStatus::Untracked,
|
||||||
|
(head, index, worktree) => TrackedStatus {
|
||||||
|
index_status: status_code(&head, &index),
|
||||||
|
worktree_status: status_code(&index, &worktree),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
};
|
||||||
|
Some((path, status))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GitRepository for FakeGitRepository {
|
impl GitRepository for FakeGitRepository {
|
||||||
fn reload_index(&self) {}
|
fn reload_index(&self) {}
|
||||||
|
|
||||||
fn load_index_text(&self, path: &Path) -> Option<String> {
|
fn load_index_text(&self, path: &RepoPath) -> Option<String> {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
state.index_contents.get(path).cloned()
|
state.index_contents.get(path).cloned()
|
||||||
}
|
}
|
||||||
@@ -360,8 +448,7 @@ impl GitRepository for FakeGitRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn dot_git_dir(&self) -> PathBuf {
|
fn dot_git_dir(&self) -> PathBuf {
|
||||||
let state = self.state.lock();
|
self.state.lock().fake_fs.dot_git_dir()
|
||||||
state.dot_git_dir.clone()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
||||||
@@ -410,20 +497,14 @@ impl GitRepository for FakeGitRepository {
|
|||||||
fn change_branch(&self, name: &str) -> Result<()> {
|
fn change_branch(&self, name: &str) -> Result<()> {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.current_branch_name = Some(name.to_owned());
|
state.current_branch_name = Some(name.to_owned());
|
||||||
state
|
state.fake_fs.repo_changed();
|
||||||
.event_emitter
|
|
||||||
.try_send(state.dot_git_dir.clone())
|
|
||||||
.expect("Dropped repo change event");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_branch(&self, name: &str) -> Result<()> {
|
fn create_branch(&self, name: &str) -> Result<()> {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.branches.insert(name.to_owned());
|
state.branches.insert(name.to_owned());
|
||||||
state
|
state.fake_fs.repo_changed();
|
||||||
.event_emitter
|
|
||||||
.try_send(state.dot_git_dir.clone())
|
|
||||||
.expect("Dropped repo change event");
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,12 +517,38 @@ impl GitRepository for FakeGitRepository {
|
|||||||
.cloned()
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
|
fn stage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
||||||
unimplemented!()
|
let mut state = self.state.lock();
|
||||||
|
for path in paths {
|
||||||
|
match state.fake_fs.read_path(path) {
|
||||||
|
Some(content) => {
|
||||||
|
state.index_contents.insert(path.clone(), content);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
state.index_contents.remove(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.fake_fs.repo_changed();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unstage_paths(&self, _paths: &[RepoPath]) -> Result<()> {
|
fn unstage_paths(&self, paths: &[RepoPath]) -> Result<()> {
|
||||||
unimplemented!()
|
let mut state = self.state.lock();
|
||||||
|
for path in paths {
|
||||||
|
let content = state.head_contents.get(path).cloned();
|
||||||
|
match content {
|
||||||
|
Some(content) => {
|
||||||
|
state.index_contents.insert(path.clone(), content);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
state.index_contents.remove(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.set_statuses();
|
||||||
|
state.fake_fs.repo_changed();
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&self, _message: &str) -> Result<()> {
|
fn commit(&self, _message: &str) -> Result<()> {
|
||||||
|
|||||||
@@ -3213,6 +3213,59 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_fake_git_repository_fs(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let fs = FakeFs::new(cx.background_executor.clone());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root",
|
||||||
|
json!({
|
||||||
|
"x": {
|
||||||
|
".git": {},
|
||||||
|
"x1.txt": "foo",
|
||||||
|
"x2.txt": "bar",
|
||||||
|
"y": {
|
||||||
|
".git": {},
|
||||||
|
"y1.txt": "baz",
|
||||||
|
"y2.txt": "qux"
|
||||||
|
},
|
||||||
|
"z.txt": "sneaky..."
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
fs.with_git_state("/root/x/.git".as_ref(), false, |repo_state| {
|
||||||
|
let all_paths = repo_state.fake_fs.all_paths().collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
all_paths,
|
||||||
|
&[
|
||||||
|
("x1.txt".into(), "foo".to_owned()),
|
||||||
|
("x2.txt".into(), "bar".to_owned()),
|
||||||
|
("z.txt".into(), "sneaky...".to_owned())
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//#[gpui::test]
|
||||||
|
//async fn test_git_status_correspondence(mut rng: StdRng, cx: &mut TestAppContext) {
|
||||||
|
// // operations
|
||||||
|
// // - take something that's staged and unstage it
|
||||||
|
// // - take something that's unstaged and stage it
|
||||||
|
// // - take something that's modified from the index and revert it
|
||||||
|
// // - take something that's unmodified from the index and modify it
|
||||||
|
// init_test(cx);
|
||||||
|
// let fake = FakeFs::new(cx.background_executor.clone());
|
||||||
|
// let initial = json!({
|
||||||
|
// "a.txt": "a",
|
||||||
|
// "b.txt": "b",
|
||||||
|
// "c.txt": "c",
|
||||||
|
// });
|
||||||
|
// fake.insert_tree("/project", initial.clone()).await;
|
||||||
|
// let real = temp_tree(json!({"project": initial}));
|
||||||
|
//}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
async fn test_private_single_file_worktree(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|||||||
Reference in New Issue
Block a user