git: Git Panel UI, continued (#22960)

TODO:

- [ ] Investigate incorrect hit target for `stage all` button
- [ ] Add top level context menu
- [ ] Add entry context menus
- [x] Show paths in list view
- [ ] For now, `enter` can just open the file
- [ ] 🐞: Hover deadzone in list caused by scrollbar
- [x] 🐞: Incorrect status/nothing shown when multiple worktrees are
added

---

This PR continues work on the feature flagged git panel.

Changes:
- Defines and wires up git panel actions & keybindings
- Re-scopes some actions from `git_ui` -> `git`.
- General git actions (StageAll, CommitChanges, ...) are scoped to
`git`.
- Git panel specific actions (Close, FocusCommitEditor, ...) are scoped
to `git_panel.
- Staging actions & UI are now connected to git!
- Unify more reusable git status into the GitState global over being
tied to the panel directly.
- Uses the new git status codepaths instead of filtering all workspace
entries

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <53574922+cole-miller@users.noreply.github.com>
Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Nate Butler 2025-01-13 11:47:09 -05:00 committed by GitHub
parent 1c6dd03e50
commit 102e70816c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1006 additions and 840 deletions

View file

@ -2179,7 +2179,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
let repo = tree.repositories().next().unwrap();
let repo = tree.repositories().iter().next().unwrap();
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
assert_eq!(
tree.status_for_file(Path::new("projects/project1/a")),
@ -2200,7 +2200,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
cx.read(|cx| {
let tree = tree.read(cx);
let repo = tree.repositories().next().unwrap();
let repo = tree.repositories().iter().next().unwrap();
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
assert_eq!(
tree.status_for_file(Path::new("projects/project2/a")),
@ -2380,8 +2380,8 @@ async fn test_file_status(cx: &mut TestAppContext) {
// Check that the right git state is observed on startup
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
assert_eq!(snapshot.repositories().count(), 1);
let repo_entry = snapshot.repositories().next().unwrap();
assert_eq!(snapshot.repositories().iter().count(), 1);
let repo_entry = snapshot.repositories().iter().next().unwrap();
assert_eq!(repo_entry.path.as_ref(), Path::new("project"));
assert!(repo_entry.location_in_repo.is_none());
@ -2554,16 +2554,16 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
// Check that the right git state is observed on startup
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repo = snapshot.repositories().next().unwrap();
let repo = snapshot.repositories().iter().next().unwrap();
let entries = repo.status().collect::<Vec<_>>();
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].status, GitFileStatus::Modified);
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Modified));
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, GitFileStatus::Untracked);
assert_eq!(entries[1].worktree_status(), Some(GitFileStatus::Untracked));
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[2].status, GitFileStatus::Deleted);
assert_eq!(entries[2].worktree_status(), Some(GitFileStatus::Deleted));
});
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
@ -2576,19 +2576,19 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repository = snapshot.repositories().next().unwrap();
let repository = snapshot.repositories().iter().next().unwrap();
let entries = repository.status().collect::<Vec<_>>();
std::assert_eq!(entries.len(), 4, "entries: {entries:?}");
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].status, GitFileStatus::Modified);
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Modified));
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, GitFileStatus::Untracked);
assert_eq!(entries[1].worktree_status(), Some(GitFileStatus::Untracked));
// Status updated
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
assert_eq!(entries[2].status, GitFileStatus::Modified);
assert_eq!(entries[2].worktree_status(), Some(GitFileStatus::Modified));
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[3].status, GitFileStatus::Deleted);
assert_eq!(entries[3].worktree_status(), Some(GitFileStatus::Deleted));
});
git_add("a.txt", &repo);
@ -2609,7 +2609,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
let repo = snapshot.repositories().next().unwrap();
let repo = snapshot.repositories().iter().next().unwrap();
let entries = repo.status().collect::<Vec<_>>();
// Deleting an untracked entry, b.txt, should leave no status
@ -2621,7 +2621,7 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
&entries
);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(entries[0].status, GitFileStatus::Deleted);
assert_eq!(entries[0].worktree_status(), Some(GitFileStatus::Deleted));
});
}
@ -2676,8 +2676,8 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
// Ensure that the git status is loaded correctly
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
assert_eq!(snapshot.repositories().count(), 1);
let repo = snapshot.repositories().next().unwrap();
assert_eq!(snapshot.repositories().iter().count(), 1);
let repo = snapshot.repositories().iter().next().unwrap();
// Path is blank because the working directory of
// the git repository is located at the root of the project
assert_eq!(repo.path.as_ref(), Path::new(""));
@ -2707,7 +2707,7 @@ async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
tree.read_with(cx, |tree, _cx| {
let snapshot = tree.snapshot();
assert!(snapshot.repositories().next().is_some());
assert!(snapshot.repositories().iter().next().is_some());
assert_eq!(snapshot.status_for_file("c.txt"), None);
assert_eq!(snapshot.status_for_file("d/e.txt"), None);