mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Added git status to the project panel, added worktree test
This commit is contained in:
parent
93f57430da
commit
e98507d8bf
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4717,6 +4717,7 @@ dependencies = [
|
||||
"futures 0.3.25",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"git2",
|
||||
"glob",
|
||||
"gpui",
|
||||
"ignore",
|
||||
|
@ -74,5 +74,6 @@ lsp = { path = "../lsp", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
rpc = { path = "../rpc", features = ["test-support"] }
|
||||
git2 = { version = "0.15", default-features = false }
|
||||
tempdir.workspace = true
|
||||
unindent.workspace = true
|
||||
|
@ -120,6 +120,25 @@ pub struct Snapshot {
|
||||
completed_scan_id: usize,
|
||||
}
|
||||
|
||||
impl Snapshot {
|
||||
pub fn repo_for(&self, path: &Path) -> Option<RepositoryEntry> {
|
||||
let mut max_len = 0;
|
||||
let mut current_candidate = None;
|
||||
for (work_directory, repo) in (&self.repository_entries).iter() {
|
||||
if repo.contains(self, path) {
|
||||
if work_directory.0.as_os_str().len() >= max_len {
|
||||
current_candidate = Some(repo);
|
||||
max_len = work_directory.0.as_os_str().len();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current_candidate.map(|entry| entry.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct RepositoryEntry {
|
||||
pub(crate) work_directory: WorkDirectoryEntry,
|
||||
@ -145,6 +164,13 @@ impl RepositoryEntry {
|
||||
pub(crate) fn contains(&self, snapshot: &Snapshot, path: &Path) -> bool {
|
||||
self.work_directory.contains(snapshot, path)
|
||||
}
|
||||
|
||||
pub fn status_for(&self, snapshot: &Snapshot, path: &Path) -> Option<GitStatus> {
|
||||
self.work_directory
|
||||
.relativize(snapshot, path)
|
||||
.and_then(|repo_path| self.statuses.get(&repo_path))
|
||||
.cloned()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RepositoryEntry> for proto::RepositoryEntry {
|
||||
@ -1560,23 +1586,6 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
impl LocalSnapshot {
|
||||
pub(crate) fn repo_for(&self, path: &Path) -> Option<RepositoryEntry> {
|
||||
let mut max_len = 0;
|
||||
let mut current_candidate = None;
|
||||
for (work_directory, repo) in (&self.repository_entries).iter() {
|
||||
if repo.contains(self, path) {
|
||||
if work_directory.0.as_os_str().len() >= max_len {
|
||||
current_candidate = Some(repo);
|
||||
max_len = work_directory.0.as_os_str().len();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current_candidate.map(|entry| entry.to_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn get_local_repo(&self, repo: &RepositoryEntry) -> Option<&LocalRepositoryEntry> {
|
||||
self.git_repositories.get(&repo.work_directory.0)
|
||||
}
|
||||
@ -3751,6 +3760,203 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[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) {
|
||||
let signature = repo.signature().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) {
|
||||
let signature = repo.signature().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");
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
|
||||
let root = temp_tree(json!({
|
||||
"project": {
|
||||
"a.txt": "a",
|
||||
"b.txt": "bb",
|
||||
},
|
||||
|
||||
}));
|
||||
|
||||
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();
|
||||
|
||||
const A_TXT: &'static str = "a.txt";
|
||||
const B_TXT: &'static str = "b.txt";
|
||||
let work_dir = root.path().join("project");
|
||||
|
||||
let mut repo = git_init(work_dir.as_path());
|
||||
git_add(Path::new(A_TXT), &repo);
|
||||
git_commit("Initial commit", &repo);
|
||||
|
||||
std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
|
||||
|
||||
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
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(), 2);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(A_TXT).into()),
|
||||
Some(&GitStatus::Modified)
|
||||
);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(B_TXT).into()),
|
||||
Some(&GitStatus::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(), 0);
|
||||
assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None);
|
||||
assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None);
|
||||
});
|
||||
|
||||
git_reset(0, &repo);
|
||||
git_remove_index(Path::new(B_TXT), &repo);
|
||||
git_stash(&mut repo);
|
||||
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();
|
||||
|
||||
|
||||
dbg!(&repo.statuses);
|
||||
|
||||
|
||||
assert_eq!(repo.statuses.iter().count(), 1);
|
||||
assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None);
|
||||
assert_eq!(
|
||||
repo.statuses.get(&Path::new(B_TXT).into()),
|
||||
Some(&GitStatus::Added)
|
||||
);
|
||||
});
|
||||
|
||||
std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
|
||||
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);
|
||||
assert_eq!(repo.statuses.get(&Path::new(A_TXT).into()), None);
|
||||
assert_eq!(repo.statuses.get(&Path::new(B_TXT).into()), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_write_file(cx: &mut TestAppContext) {
|
||||
let dir = temp_tree(json!({
|
||||
|
@ -13,10 +13,10 @@ use gpui::{
|
||||
keymap_matcher::KeymapContext,
|
||||
platform::{CursorStyle, MouseButton, PromptLevel},
|
||||
AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
ViewHandle, WeakViewHandle, color::Color,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId, repository::GitStatus};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
@ -86,6 +86,7 @@ pub struct EntryDetails {
|
||||
is_editing: bool,
|
||||
is_processing: bool,
|
||||
is_cut: bool,
|
||||
git_status: Option<GitStatus>
|
||||
}
|
||||
|
||||
actions!(
|
||||
@ -1008,6 +1009,13 @@ impl ProjectPanel {
|
||||
|
||||
let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
|
||||
for entry in &visible_worktree_entries[entry_range] {
|
||||
let path = &entry.path;
|
||||
let status = snapshot.repo_for(path)
|
||||
.and_then(|entry| {
|
||||
entry.status_for(&snapshot, path)
|
||||
});
|
||||
|
||||
|
||||
let mut details = EntryDetails {
|
||||
filename: entry
|
||||
.path
|
||||
@ -1028,6 +1036,7 @@ impl ProjectPanel {
|
||||
is_cut: self
|
||||
.clipboard_entry
|
||||
.map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
|
||||
git_status: status
|
||||
};
|
||||
|
||||
if let Some(edit_state) = &self.edit_state {
|
||||
@ -1069,6 +1078,15 @@ impl ProjectPanel {
|
||||
let kind = details.kind;
|
||||
let show_editor = details.is_editing && !details.is_processing;
|
||||
|
||||
let git_color = details.git_status.as_ref().and_then(|status| {
|
||||
match status {
|
||||
GitStatus::Added => Some(Color::green()),
|
||||
GitStatus::Modified => Some(Color::blue()),
|
||||
GitStatus::Conflict => Some(Color::red()),
|
||||
GitStatus::Untracked => None,
|
||||
}
|
||||
}).unwrap_or(Color::transparent_black());
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
if kind == EntryKind::Dir {
|
||||
@ -1107,6 +1125,7 @@ impl ProjectPanel {
|
||||
.with_height(style.height)
|
||||
.contained()
|
||||
.with_style(row_container_style)
|
||||
.with_background_color(git_color)
|
||||
.with_padding_left(padding)
|
||||
.into_any_named("project panel entry visual element")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user