Added git status to the project panel, added worktree test

This commit is contained in:
Mikayla Maki 2023-05-09 14:42:51 -07:00 committed by Mikayla Maki
parent 93f57430da
commit e98507d8bf
No known key found for this signature in database
4 changed files with 246 additions and 19 deletions

1
Cargo.lock generated
View File

@ -4717,6 +4717,7 @@ dependencies = [
"futures 0.3.25",
"fuzzy",
"git",
"git2",
"glob",
"gpui",
"ignore",

View File

@ -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

View File

@ -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!({

View File

@ -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")
}