mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Rearrange git tests in worktree
Add support for renaming work directories
This commit is contained in:
parent
623a177fe6
commit
c795c9b844
@ -42,7 +42,6 @@ use language::{
|
||||
};
|
||||
use project::ProjectPath;
|
||||
use smallvec::SmallVec;
|
||||
use text::Point;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cmp::{self, Ordering},
|
||||
@ -51,6 +50,7 @@ use std::{
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use text::Point;
|
||||
use workspace::{item::Item, GitGutterSetting, WorkspaceSettings};
|
||||
|
||||
enum FoldMarkers {}
|
||||
@ -1059,8 +1059,10 @@ impl EditorElement {
|
||||
.buffer_snapshot
|
||||
.git_diff_hunks_in_range(0..(max_row.floor() as u32), false)
|
||||
{
|
||||
let start_display = Point::new(hunk.buffer_range.start, 0).to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let end_display = Point::new(hunk.buffer_range.end, 0).to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let start_display = Point::new(hunk.buffer_range.start, 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let end_display = Point::new(hunk.buffer_range.end, 0)
|
||||
.to_display_point(&layout.position_map.snapshot.display_snapshot);
|
||||
let start_y = y_for_row(start_display.row() as f32);
|
||||
let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
|
||||
y_for_row((end_display.row() + 1) as f32)
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
use git::diff::{DiffHunk, DiffHunkStatus};
|
||||
@ -78,10 +77,7 @@ pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) ->
|
||||
} else {
|
||||
let start = hunk_start_point.to_display_point(snapshot).row();
|
||||
|
||||
let hunk_end_row_inclusive = hunk
|
||||
.buffer_range
|
||||
.end
|
||||
.max(hunk.buffer_range.start);
|
||||
let hunk_end_row_inclusive = hunk.buffer_range.end.max(hunk.buffer_range.start);
|
||||
let hunk_end_point = Point::new(hunk_end_row_inclusive, 0);
|
||||
let end = hunk_end_point.to_display_point(snapshot).row();
|
||||
|
||||
|
@ -150,13 +150,6 @@ impl RepositoryEntry {
|
||||
.map(|entry| RepositoryWorkDirectory(entry.path.clone()))
|
||||
}
|
||||
|
||||
pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option<GitFileStatus> {
|
||||
self.work_directory
|
||||
.relativize(snapshot, path)
|
||||
.and_then(|repo_path| self.statuses.get(&repo_path))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn status_for_path(&self, snapshot: &Snapshot, path: &Path) -> Option<GitFileStatus> {
|
||||
self.work_directory
|
||||
.relativize(snapshot, path)
|
||||
@ -182,6 +175,14 @@ impl RepositoryEntry {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn status_for_file(&self, snapshot: &Snapshot, path: &Path) -> Option<GitFileStatus> {
|
||||
self.work_directory
|
||||
.relativize(snapshot, path)
|
||||
.and_then(|repo_path| (&self.statuses).get(&repo_path))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
pub fn build_update(&self, other: &Self) -> proto::RepositoryEntry {
|
||||
let mut updated_statuses: Vec<proto::StatusEntry> = Vec::new();
|
||||
let mut removed_statuses: Vec<String> = Vec::new();
|
||||
@ -1638,6 +1639,11 @@ impl Snapshot {
|
||||
.map(|(path, entry)| (&path.0, entry))
|
||||
}
|
||||
|
||||
/// Get the repository whose work directory contains the given path.
|
||||
pub fn repository_for_work_directory(&self, path: &Path) -> Option<RepositoryEntry> {
|
||||
self.repository_entries.get(&RepositoryWorkDirectory(path.into())).cloned()
|
||||
}
|
||||
|
||||
/// Get the repository whose work directory contains the given path.
|
||||
pub fn repository_for_path(&self, path: &Path) -> Option<RepositoryEntry> {
|
||||
let mut max_len = 0;
|
||||
@ -1653,7 +1659,7 @@ impl Snapshot {
|
||||
}
|
||||
}
|
||||
|
||||
current_candidate.map(|entry| entry.to_owned())
|
||||
current_candidate.cloned()
|
||||
}
|
||||
|
||||
/// Given an ordered iterator of entries, returns an iterator of those entries,
|
||||
@ -3105,6 +3111,17 @@ impl BackgroundScanner {
|
||||
.any(|component| component.as_os_str() == *DOT_GIT)
|
||||
{
|
||||
let scan_id = snapshot.scan_id;
|
||||
|
||||
if let Some(repository) = snapshot.repository_for_work_directory(path) {
|
||||
let entry = repository.work_directory.0;
|
||||
snapshot.git_repositories.remove(&entry);
|
||||
snapshot
|
||||
.snapshot
|
||||
.repository_entries
|
||||
.remove(&RepositoryWorkDirectory(path.into()));
|
||||
return Some(());
|
||||
}
|
||||
|
||||
let repo = snapshot.repository_for_path(&path)?;
|
||||
|
||||
let repo_path = repo.work_directory.relativize(&snapshot, &path)?;
|
||||
@ -3975,6 +3992,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||
// .gitignores are handled explicitly by Zed and do not use the git
|
||||
// machinery that the git_tests module checks
|
||||
let parent_dir = temp_tree(json!({
|
||||
".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
|
||||
"tree": {
|
||||
@ -4052,402 +4071,6 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||
let root = temp_tree(json!({
|
||||
"c.txt": "",
|
||||
"dir1": {
|
||||
".git": {},
|
||||
"deps": {
|
||||
"dep1": {
|
||||
".git": {},
|
||||
"src": {
|
||||
"a.txt": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"src": {
|
||||
"b.txt": ""
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
|
||||
assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
|
||||
|
||||
let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
|
||||
assert_eq!(
|
||||
entry
|
||||
.work_directory(tree)
|
||||
.map(|directory| directory.as_ref().to_owned()),
|
||||
Some(Path::new("dir1").to_owned())
|
||||
);
|
||||
|
||||
let entry = tree
|
||||
.repository_for_path("dir1/deps/dep1/src/a.txt".as_ref())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
entry
|
||||
.work_directory(tree)
|
||||
.map(|directory| directory.as_ref().to_owned()),
|
||||
Some(Path::new("dir1/deps/dep1").to_owned())
|
||||
);
|
||||
|
||||
let entries = tree.files(false, 0);
|
||||
|
||||
let paths_with_repos = tree
|
||||
.entries_with_repositories(entries)
|
||||
.map(|(entry, repo)| {
|
||||
(
|
||||
entry.path.as_ref(),
|
||||
repo.and_then(|repo| {
|
||||
repo.work_directory(&tree)
|
||||
.map(|work_directory| work_directory.0.to_path_buf())
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
paths_with_repos,
|
||||
&[
|
||||
(Path::new("c.txt"), None),
|
||||
(
|
||||
Path::new("dir1/deps/dep1/src/a.txt"),
|
||||
Some(Path::new("dir1/deps/dep1").into())
|
||||
),
|
||||
(Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
let repo_update_events = Arc::new(Mutex::new(vec![]));
|
||||
tree.update(cx, |_, cx| {
|
||||
let repo_update_events = repo_update_events.clone();
|
||||
cx.subscribe(&tree, move |_, _, event, _| {
|
||||
if let Event::UpdatedGitRepositories(update) = event {
|
||||
repo_update_events.lock().push(update.clone());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
assert_eq!(
|
||||
repo_update_events.lock()[0]
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<Arc<Path>>>(),
|
||||
vec![Path::new("dir1").into()]
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
|
||||
assert!(tree
|
||||
.repository_for_path("dir1/src/b.txt".as_ref())
|
||||
.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_status(cx: &mut TestAppContext) {
|
||||
#[track_caller]
|
||||
fn git_init(path: &Path) -> git2::Repository {
|
||||
git2::Repository::init(path).expect("Failed to initialize git repository")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_add(path: &Path, repo: &git2::Repository) {
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.add_path(path).expect("Failed to add a.txt");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_remove_index(path: &Path, repo: &git2::Repository) {
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.remove_path(path).expect("Failed to add a.txt");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_commit(msg: &'static str, repo: &git2::Repository) {
|
||||
use git2::Signature;
|
||||
|
||||
let signature = Signature::now("test", "test@zed.dev").unwrap();
|
||||
let oid = repo.index().unwrap().write_tree().unwrap();
|
||||
let tree = repo.find_tree(oid).unwrap();
|
||||
if let Some(head) = repo.head().ok() {
|
||||
let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
|
||||
|
||||
let parent_commit = parent_obj.as_commit().unwrap();
|
||||
|
||||
repo.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
&[parent_commit],
|
||||
)
|
||||
.expect("Failed to commit with parent");
|
||||
} else {
|
||||
repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
|
||||
.expect("Failed to commit");
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_stash(repo: &mut git2::Repository) {
|
||||
use git2::Signature;
|
||||
|
||||
let signature = Signature::now("test", "test@zed.dev").unwrap();
|
||||
repo.stash_save(&signature, "N/A", None)
|
||||
.expect("Failed to stash");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_reset(offset: usize, repo: &git2::Repository) {
|
||||
let head = repo.head().expect("Couldn't get repo head");
|
||||
let object = head.peel(git2::ObjectType::Commit).unwrap();
|
||||
let commit = object.as_commit().unwrap();
|
||||
let new_head = commit
|
||||
.parents()
|
||||
.inspect(|parnet| {
|
||||
parnet.message();
|
||||
})
|
||||
.skip(offset)
|
||||
.next()
|
||||
.expect("Not enough history");
|
||||
repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
|
||||
.expect("Could not reset");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[track_caller]
|
||||
fn git_status(repo: &git2::Repository) -> HashMap<String, git2::Status> {
|
||||
repo.statuses(None)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|status| (status.path().unwrap().to_string(), status.status()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
const IGNORE_RULE: &'static str = "**/target";
|
||||
|
||||
let root = temp_tree(json!({
|
||||
"project": {
|
||||
"a.txt": "a",
|
||||
"b.txt": "bb",
|
||||
"c": {
|
||||
"d": {
|
||||
"e.txt": "eee"
|
||||
}
|
||||
},
|
||||
"f.txt": "ffff",
|
||||
"target": {
|
||||
"build_file": "???"
|
||||
},
|
||||
".gitignore": IGNORE_RULE
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
const A_TXT: &'static str = "a.txt";
|
||||
const B_TXT: &'static str = "b.txt";
|
||||
const E_TXT: &'static str = "c/d/e.txt";
|
||||
const F_TXT: &'static str = "f.txt";
|
||||
const DOTGITIGNORE: &'static str = ".gitignore";
|
||||
const BUILD_FILE: &'static str = "target/build_file";
|
||||
|
||||
let work_dir = root.path().join("project");
|
||||
let mut repo = git_init(work_dir.as_path());
|
||||
repo.add_ignore_rule(IGNORE_RULE).unwrap();
|
||||
git_add(Path::new(A_TXT), &repo);
|
||||
git_add(Path::new(E_TXT), &repo);
|
||||
git_add(Path::new(DOTGITIGNORE), &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
|
||||
std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
// Check that the right git state is observed on startup
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repository_entries.iter().count(), 1);
|
||||
let (dir, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
assert_eq!(dir.0.as_ref(), Path::new("project"));
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 3);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(A_TXT).into()),
|
||||
Some(&GitFileStatus::Modified)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(B_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(F_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
|
||||
git_add(Path::new(A_TXT), &repo);
|
||||
git_add(Path::new(B_TXT), &repo);
|
||||
git_commit("Committing modified and added", &repo);
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
// Check that repo only changes are tracked
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 1);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(F_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
|
||||
git_reset(0, &repo);
|
||||
git_remove_index(Path::new(B_TXT), &repo);
|
||||
git_stash(&mut repo);
|
||||
std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
|
||||
std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
// Check that more complex repo changes are tracked
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 3);
|
||||
assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(B_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(E_TXT).into()),
|
||||
Some(&GitFileStatus::Modified)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(F_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
|
||||
std::fs::remove_dir_all(work_dir.join("c")).unwrap();
|
||||
std::fs::write(
|
||||
work_dir.join(DOTGITIGNORE),
|
||||
[IGNORE_RULE, "f.txt"].join("\n"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
git_add(Path::new(DOTGITIGNORE), &repo);
|
||||
git_commit("Committing modified git ignore", &repo);
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
// Check that non-repo behavior is tracked
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 0);
|
||||
});
|
||||
|
||||
let mut renamed_dir_name = "first_directory/second_directory";
|
||||
const RENAMED_FILE: &'static str = "rf.txt";
|
||||
|
||||
std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
|
||||
std::fs::write(
|
||||
work_dir.join(renamed_dir_name).join(RENAMED_FILE),
|
||||
"new-contents",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 1);
|
||||
assert_eq!(
|
||||
repo.statuses
|
||||
.get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
|
||||
renamed_dir_name = "new_first_directory/second_directory";
|
||||
|
||||
std::fs::rename(
|
||||
work_dir.join("first_directory"),
|
||||
work_dir.join("new_first_directory"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 1);
|
||||
assert_eq!(
|
||||
repo.statuses
|
||||
.get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_write_file(cx: &mut TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
@ -5100,4 +4723,474 @@ mod tests {
|
||||
paths
|
||||
}
|
||||
}
|
||||
|
||||
mod git_tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||
let root = temp_tree(json!({
|
||||
"projects": {
|
||||
"project1": {
|
||||
"a": "",
|
||||
"b": "",
|
||||
}
|
||||
},
|
||||
|
||||
}));
|
||||
let root_path = root.path();
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
root_path,
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let repo = git_init(&root_path.join("projects/project1"));
|
||||
git_add("a", &repo);
|
||||
git_commit("init", &repo);
|
||||
std::fs::write(root_path.join("projects/project1/a"), "aa").ok();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let (work_dir, repo) = tree.repositories().next().unwrap();
|
||||
assert_eq!(work_dir.as_ref(), Path::new("projects/project1"));
|
||||
assert_eq!(
|
||||
repo.status_for_file(tree, Path::new("projects/project1/a")),
|
||||
Some(GitFileStatus::Modified)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_file(tree, Path::new("projects/project1/b")),
|
||||
Some(GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
dbg!("RENAMING");
|
||||
std::fs::rename(
|
||||
root_path.join("projects/project1"),
|
||||
root_path.join("projects/project2"),
|
||||
)
|
||||
.ok();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
cx.read(|cx| {
|
||||
let tree = tree.read(cx);
|
||||
let (work_dir, repo) = tree.repositories().next().unwrap();
|
||||
assert_eq!(work_dir.as_ref(), Path::new("projects/project2"));
|
||||
assert_eq!(
|
||||
repo.status_for_file(tree, Path::new("projects/project2/a")),
|
||||
Some(GitFileStatus::Modified)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.status_for_file(tree, Path::new("projects/project2/b")),
|
||||
Some(GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||
let root = temp_tree(json!({
|
||||
"c.txt": "",
|
||||
"dir1": {
|
||||
".git": {},
|
||||
"deps": {
|
||||
"dep1": {
|
||||
".git": {},
|
||||
"src": {
|
||||
"a.txt": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"src": {
|
||||
"b.txt": ""
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
|
||||
assert!(tree.repository_for_path("c.txt".as_ref()).is_none());
|
||||
|
||||
let entry = tree.repository_for_path("dir1/src/b.txt".as_ref()).unwrap();
|
||||
assert_eq!(
|
||||
entry
|
||||
.work_directory(tree)
|
||||
.map(|directory| directory.as_ref().to_owned()),
|
||||
Some(Path::new("dir1").to_owned())
|
||||
);
|
||||
|
||||
let entry = tree.repository_for_path("dir1/deps/dep1/src/a.txt".as_ref()).unwrap();
|
||||
assert_eq!(
|
||||
entry
|
||||
.work_directory(tree)
|
||||
.map(|directory| directory.as_ref().to_owned()),
|
||||
Some(Path::new("dir1/deps/dep1").to_owned())
|
||||
);
|
||||
|
||||
let entries = tree.files(false, 0);
|
||||
|
||||
let paths_with_repos = tree
|
||||
.entries_with_repositories(entries)
|
||||
.map(|(entry, repo)| {
|
||||
(
|
||||
entry.path.as_ref(),
|
||||
repo.and_then(|repo| {
|
||||
repo.work_directory(&tree)
|
||||
.map(|work_directory| work_directory.0.to_path_buf())
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
paths_with_repos,
|
||||
&[
|
||||
(Path::new("c.txt"), None),
|
||||
(
|
||||
Path::new("dir1/deps/dep1/src/a.txt"),
|
||||
Some(Path::new("dir1/deps/dep1").into())
|
||||
),
|
||||
(Path::new("dir1/src/b.txt"), Some(Path::new("dir1").into())),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
let repo_update_events = Arc::new(Mutex::new(vec![]));
|
||||
tree.update(cx, |_, cx| {
|
||||
let repo_update_events = repo_update_events.clone();
|
||||
cx.subscribe(&tree, move |_, _, event, _| {
|
||||
if let Event::UpdatedGitRepositories(update) = event {
|
||||
repo_update_events.lock().push(update.clone());
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
std::fs::write(root.path().join("dir1/.git/random_new_file"), "hello").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
assert_eq!(
|
||||
repo_update_events.lock()[0]
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<Arc<Path>>>(),
|
||||
vec![Path::new("dir1").into()]
|
||||
);
|
||||
|
||||
std::fs::remove_dir_all(root.path().join("dir1/.git")).unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let tree = tree.as_local().unwrap();
|
||||
|
||||
assert!(tree.repository_for_path("dir1/src/b.txt".as_ref()).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_git_status(cx: &mut TestAppContext) {
|
||||
const IGNORE_RULE: &'static str = "**/target";
|
||||
|
||||
let root = temp_tree(json!({
|
||||
"project": {
|
||||
"a.txt": "a",
|
||||
"b.txt": "bb",
|
||||
"c": {
|
||||
"d": {
|
||||
"e.txt": "eee"
|
||||
}
|
||||
},
|
||||
"f.txt": "ffff",
|
||||
"target": {
|
||||
"build_file": "???"
|
||||
},
|
||||
".gitignore": IGNORE_RULE
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
let http_client = FakeHttpClient::with_404_response();
|
||||
let client = cx.read(|cx| Client::new(http_client, cx));
|
||||
let tree = Worktree::local(
|
||||
client,
|
||||
root.path(),
|
||||
true,
|
||||
Arc::new(RealFs),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
|
||||
const A_TXT: &'static str = "a.txt";
|
||||
const B_TXT: &'static str = "b.txt";
|
||||
const E_TXT: &'static str = "c/d/e.txt";
|
||||
const F_TXT: &'static str = "f.txt";
|
||||
const DOTGITIGNORE: &'static str = ".gitignore";
|
||||
const BUILD_FILE: &'static str = "target/build_file";
|
||||
|
||||
let work_dir = root.path().join("project");
|
||||
let mut repo = git_init(work_dir.as_path());
|
||||
repo.add_ignore_rule(IGNORE_RULE).unwrap();
|
||||
git_add(Path::new(A_TXT), &repo);
|
||||
git_add(Path::new(E_TXT), &repo);
|
||||
git_add(Path::new(DOTGITIGNORE), &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
|
||||
std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
// Check that the right git state is observed on startup
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
assert_eq!(snapshot.repository_entries.iter().count(), 1);
|
||||
let (dir, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
assert_eq!(dir.0.as_ref(), Path::new("project"));
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 3);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(A_TXT).into()),
|
||||
Some(&GitFileStatus::Modified)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(B_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(F_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
|
||||
git_add(Path::new(A_TXT), &repo);
|
||||
git_add(Path::new(B_TXT), &repo);
|
||||
git_commit("Committing modified and added", &repo);
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
// Check that repo only changes are tracked
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 1);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(F_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
|
||||
git_reset(0, &repo);
|
||||
git_remove_index(Path::new(B_TXT), &repo);
|
||||
git_stash(&mut repo);
|
||||
std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
|
||||
std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
// Check that more complex repo changes are tracked
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 3);
|
||||
assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(B_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(E_TXT).into()),
|
||||
Some(&GitFileStatus::Modified)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(F_TXT).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
|
||||
std::fs::remove_dir_all(work_dir.join("c")).unwrap();
|
||||
std::fs::write(
|
||||
work_dir.join(DOTGITIGNORE),
|
||||
[IGNORE_RULE, "f.txt"].join("\n"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
git_add(Path::new(DOTGITIGNORE), &repo);
|
||||
git_commit("Committing modified git ignore", &repo);
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
// Check that non-repo behavior is tracked
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 0);
|
||||
});
|
||||
|
||||
let mut renamed_dir_name = "first_directory/second_directory";
|
||||
const RENAMED_FILE: &'static str = "rf.txt";
|
||||
|
||||
std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
|
||||
std::fs::write(
|
||||
work_dir.join(renamed_dir_name).join(RENAMED_FILE),
|
||||
"new-contents",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 1);
|
||||
assert_eq!(
|
||||
repo.statuses
|
||||
.get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
|
||||
renamed_dir_name = "new_first_directory/second_directory";
|
||||
|
||||
std::fs::rename(
|
||||
work_dir.join("first_directory"),
|
||||
work_dir.join("new_first_directory"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
tree.read_with(cx, |tree, _cx| {
|
||||
let snapshot = tree.snapshot();
|
||||
let (_, repo) = snapshot.repository_entries.iter().next().unwrap();
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 1);
|
||||
assert_eq!(
|
||||
repo.statuses
|
||||
.get(&Path::new(renamed_dir_name).join(RENAMED_FILE).into()),
|
||||
Some(&GitFileStatus::Added)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_init(path: &Path) -> git2::Repository {
|
||||
git2::Repository::init(path).expect("Failed to initialize git repository")
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
|
||||
let path = path.as_ref();
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.add_path(path).expect("Failed to add a.txt");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_remove_index(path: &Path, repo: &git2::Repository) {
|
||||
let mut index = repo.index().expect("Failed to get index");
|
||||
index.remove_path(path).expect("Failed to add a.txt");
|
||||
index.write().expect("Failed to write index");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_commit(msg: &'static str, repo: &git2::Repository) {
|
||||
use git2::Signature;
|
||||
|
||||
let signature = Signature::now("test", "test@zed.dev").unwrap();
|
||||
let oid = repo.index().unwrap().write_tree().unwrap();
|
||||
let tree = repo.find_tree(oid).unwrap();
|
||||
if let Some(head) = repo.head().ok() {
|
||||
let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
|
||||
|
||||
let parent_commit = parent_obj.as_commit().unwrap();
|
||||
|
||||
repo.commit(
|
||||
Some("HEAD"),
|
||||
&signature,
|
||||
&signature,
|
||||
msg,
|
||||
&tree,
|
||||
&[parent_commit],
|
||||
)
|
||||
.expect("Failed to commit with parent");
|
||||
} else {
|
||||
repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
|
||||
.expect("Failed to commit");
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_stash(repo: &mut git2::Repository) {
|
||||
use git2::Signature;
|
||||
|
||||
let signature = Signature::now("test", "test@zed.dev").unwrap();
|
||||
repo.stash_save(&signature, "N/A", None)
|
||||
.expect("Failed to stash");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn git_reset(offset: usize, repo: &git2::Repository) {
|
||||
let head = repo.head().expect("Couldn't get repo head");
|
||||
let object = head.peel(git2::ObjectType::Commit).unwrap();
|
||||
let commit = object.as_commit().unwrap();
|
||||
let new_head = commit
|
||||
.parents()
|
||||
.inspect(|parnet| {
|
||||
parnet.message();
|
||||
})
|
||||
.skip(offset)
|
||||
.next()
|
||||
.expect("Not enough history");
|
||||
repo.reset(&new_head.as_object(), git2::ResetType::Soft, None)
|
||||
.expect("Could not reset");
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[track_caller]
|
||||
fn git_status(repo: &git2::Repository) -> HashMap<String, git2::Status> {
|
||||
repo.statuses(None)
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|status| (status.path().unwrap().to_string(), status.status()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user