git: Implement commit creation (#23263)

- [x] Basic implementation
- [x] Disable commit buttons when committing is not possible (empty
message, no changes)
- [x] Upgrade GitSummary to efficiently figure out whether there are any
staged changes
- [x] Make CommitAll work
- [x] Surface errors with toasts
  - [x] Channel shutdown
  - [x] Empty commit message or no changes
  - [x] Failed git operations
- [x] Fix added files no longer appearing correctly in the project panel
(GitSummary breakage)
- [x] Fix handling of commit message

Release Notes:

- N/A

---------

Co-authored-by: Nate <nate@zed.dev>
This commit is contained in:
Cole Miller 2025-01-17 13:51:20 -05:00 committed by GitHub
parent 3767e7e5f0
commit 5da67899b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 387 additions and 198 deletions

View file

@ -231,6 +231,10 @@ impl RepositoryEntry {
self.statuses_by_path.summary().item_summary.count
}
pub fn status_summary(&self) -> GitSummary {
self.statuses_by_path.summary().item_summary
}
pub fn status_for_path(&self, path: &RepoPath) -> Option<StatusEntry> {
self.statuses_by_path
.get(&PathKey(path.0.clone()), &())

View file

@ -6,7 +6,8 @@ use anyhow::Result;
use fs::{FakeFs, Fs, RealFs, RemoveOptions};
use git::{
status::{
FileStatus, GitSummary, StatusCode, TrackedStatus, UnmergedStatus, UnmergedStatusCode,
FileStatus, GitSummary, StatusCode, TrackedStatus, TrackedSummary, UnmergedStatus,
UnmergedStatusCode,
},
GITIGNORE,
};
@ -745,7 +746,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
Path::new("/root/tree/.git"),
&[(
Path::new("tracked-dir/tracked-file2"),
FileStatus::worktree(StatusCode::Added),
StatusCode::Added.index(),
)],
);
@ -830,7 +831,7 @@ async fn test_update_gitignore(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_working_copy_change(
Path::new("/root/.git"),
&[(Path::new("b.txt"), FileStatus::worktree(StatusCode::Added))],
&[(Path::new("b.txt"), StatusCode::Added.index())],
);
cx.executor().run_until_parked();
@ -1500,10 +1501,7 @@ async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) {
// detected.
fs.set_status_for_repo_via_git_operation(
Path::new("/root/.git"),
&[(
Path::new("b/c.txt"),
FileStatus::worktree(StatusCode::Modified),
)],
&[(Path::new("b/c.txt"), StatusCode::Modified.index())],
);
cx.executor().run_until_parked();
@ -2199,7 +2197,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
assert_eq!(repo.path.as_ref(), Path::new("projects/project1"));
assert_eq!(
tree.status_for_file(Path::new("projects/project1/a")),
Some(FileStatus::worktree(StatusCode::Modified)),
Some(StatusCode::Modified.worktree()),
);
assert_eq!(
tree.status_for_file(Path::new("projects/project1/b")),
@ -2220,7 +2218,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
assert_eq!(repo.path.as_ref(), Path::new("projects/project2"));
assert_eq!(
tree.status_for_file(Path::new("projects/project2/a")),
Some(FileStatus::worktree(StatusCode::Modified)),
Some(StatusCode::Modified.worktree()),
);
assert_eq!(
tree.status_for_file(Path::new("projects/project2/b")),
@ -2421,7 +2419,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
let snapshot = tree.snapshot();
assert_eq!(
snapshot.status_for_file(project_path.join(A_TXT)),
Some(FileStatus::worktree(StatusCode::Modified)),
Some(StatusCode::Modified.worktree()),
);
});
@ -2463,7 +2461,7 @@ async fn test_file_status(cx: &mut TestAppContext) {
);
assert_eq!(
snapshot.status_for_file(project_path.join(E_TXT)),
Some(FileStatus::worktree(StatusCode::Modified)),
Some(StatusCode::Modified.worktree()),
);
});
@ -2575,14 +2573,11 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
assert_eq!(
entries[0].status,
FileStatus::worktree(StatusCode::Modified)
);
assert_eq!(entries[0].status, StatusCode::Modified.worktree());
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, FileStatus::Untracked);
assert_eq!(entries[2].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[2].status, FileStatus::worktree(StatusCode::Deleted));
assert_eq!(entries[2].status, StatusCode::Deleted.worktree());
});
std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
@ -2600,20 +2595,14 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
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,
FileStatus::worktree(StatusCode::Modified)
);
assert_eq!(entries[0].status, StatusCode::Modified.worktree());
assert_eq!(entries[1].repo_path.as_ref(), Path::new("b.txt"));
assert_eq!(entries[1].status, FileStatus::Untracked);
// Status updated
assert_eq!(entries[2].repo_path.as_ref(), Path::new("c.txt"));
assert_eq!(
entries[2].status,
FileStatus::worktree(StatusCode::Modified)
);
assert_eq!(entries[2].status, StatusCode::Modified.worktree());
assert_eq!(entries[3].repo_path.as_ref(), Path::new("d.txt"));
assert_eq!(entries[3].status, FileStatus::worktree(StatusCode::Deleted));
assert_eq!(entries[3].status, StatusCode::Deleted.worktree());
});
git_add("a.txt", &repo);
@ -2646,7 +2635,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, FileStatus::worktree(StatusCode::Deleted));
assert_eq!(entries[0].status, StatusCode::Deleted.worktree());
});
}
@ -2769,11 +2758,8 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[
(
Path::new("x2.txt"),
FileStatus::worktree(StatusCode::Modified),
),
(Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
(Path::new("x2.txt"), StatusCode::Modified.index()),
(Path::new("z.txt"), StatusCode::Added.index()),
],
);
fs.set_status_for_repo_via_git_operation(
@ -2782,7 +2768,7 @@ async fn test_traverse_with_git_status(cx: &mut TestAppContext) {
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
&[(Path::new("z2.txt"), StatusCode::Added.index())],
);
let tree = Worktree::local(
@ -2862,14 +2848,8 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/.git"),
&[
(
Path::new("a/b/c1.txt"),
FileStatus::worktree(StatusCode::Added),
),
(
Path::new("a/d/e2.txt"),
FileStatus::worktree(StatusCode::Modified),
),
(Path::new("a/b/c1.txt"), StatusCode::Added.index()),
(Path::new("a/d/e2.txt"), StatusCode::Modified.index()),
(Path::new("g/h2.txt"), CONFLICT),
],
);
@ -2971,24 +2951,18 @@ async fn test_propagate_statuses_for_repos_under_project(cx: &mut TestAppContext
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[(Path::new("x1.txt"), FileStatus::worktree(StatusCode::Added))],
&[(Path::new("x1.txt"), StatusCode::Added.index())],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/y/.git"),
&[
(Path::new("y1.txt"), CONFLICT),
(
Path::new("y2.txt"),
FileStatus::worktree(StatusCode::Modified),
),
(Path::new("y2.txt"), StatusCode::Modified.index()),
],
);
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(
Path::new("z2.txt"),
FileStatus::worktree(StatusCode::Modified),
)],
&[(Path::new("z2.txt"), StatusCode::Modified.index())],
);
let tree = Worktree::local(
@ -3081,11 +3055,8 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/x/.git"),
&[
(
Path::new("x2.txt"),
FileStatus::worktree(StatusCode::Modified),
),
(Path::new("z.txt"), FileStatus::worktree(StatusCode::Added)),
(Path::new("x2.txt"), StatusCode::Modified.index()),
(Path::new("z.txt"), StatusCode::Added.index()),
],
);
fs.set_status_for_repo_via_git_operation(
@ -3095,7 +3066,7 @@ async fn test_propagate_statuses_for_nested_repos(cx: &mut TestAppContext) {
fs.set_status_for_repo_via_git_operation(
Path::new("/root/z/.git"),
&[(Path::new("z2.txt"), FileStatus::worktree(StatusCode::Added))],
&[(Path::new("z2.txt"), StatusCode::Added.index())],
);
let tree = Worktree::local(
@ -3227,12 +3198,12 @@ fn check_git_statuses(snapshot: &Snapshot, expected_statuses: &[(&Path, GitSumma
}
const ADDED: GitSummary = GitSummary {
added: 1,
index: TrackedSummary::ADDED,
count: 1,
..GitSummary::UNCHANGED
};
const MODIFIED: GitSummary = GitSummary {
modified: 1,
index: TrackedSummary::MODIFIED,
count: 1,
..GitSummary::UNCHANGED
};
@ -3378,15 +3349,15 @@ fn init_test(cx: &mut gpui::TestAppContext) {
fn assert_entry_git_state(
tree: &Worktree,
path: &str,
worktree_status: Option<StatusCode>,
index_status: Option<StatusCode>,
is_ignored: bool,
) {
let entry = tree.entry_for_path(path).expect("entry {path} not found");
let status = tree.status_for_file(Path::new(path));
let expected = worktree_status.map(|worktree_status| {
let expected = index_status.map(|index_status| {
TrackedStatus {
worktree_status,
index_status: StatusCode::Unmodified,
index_status,
worktree_status: StatusCode::Unmodified,
}
.into()
});