@@ -62,6 +62,13 @@ impl FileStatus {
})
}
+ pub const fn index(index_status: StatusCode) -> Self {
+ FileStatus::Tracked(TrackedStatus {
+ worktree_status: StatusCode::Unmodified,
+ index_status,
+ })
+ }
+
/// Generate a FileStatus Code from a byte pair, as described in
/// https://git-scm.com/docs/git-status#_output
///
@@ -454,6 +461,26 @@ impl GitStatus {
})
.collect::<Vec<_>>();
entries.sort_unstable_by(|(a, _), (b, _)| a.cmp(&b));
+ // When a file exists in HEAD, is deleted in the index, and exists again in the working copy,
+ // git produces two lines for it, one reading `D ` (deleted in index, unmodified in working copy)
+ // and the other reading `??` (untracked). Merge these two into the equivalent of `DA`.
+ entries.dedup_by(|(a, a_status), (b, b_status)| {
+ const INDEX_DELETED: FileStatus = FileStatus::index(StatusCode::Deleted);
+ if a.ne(&b) {
+ return false;
+ }
+ match (*a_status, *b_status) {
+ (INDEX_DELETED, FileStatus::Untracked) | (FileStatus::Untracked, INDEX_DELETED) => {
+ *b_status = TrackedStatus {
+ index_status: StatusCode::Deleted,
+ worktree_status: StatusCode::Added,
+ }
+ .into();
+ }
+ _ => panic!("Unexpected duplicated status entries: {a_status:?} and {b_status:?}"),
+ }
+ true
+ });
Ok(Self {
entries: entries.into(),
})
@@ -2639,6 +2639,62 @@ async fn test_git_repository_status(cx: &mut TestAppContext) {
});
}
+#[gpui::test]
+async fn test_git_status_postprocessing(cx: &mut TestAppContext) {
+ init_test(cx);
+ cx.executor().allow_parking();
+
+ let root = temp_tree(json!({
+ "project": {
+ "sub": {},
+ "a.txt": "",
+ },
+ }));
+
+ let work_dir = root.path().join("project");
+ let repo = git_init(work_dir.as_path());
+ // a.txt exists in HEAD and the working copy but is deleted in the index.
+ git_add("a.txt", &repo);
+ git_commit("Initial commit", &repo);
+ git_remove_index("a.txt".as_ref(), &repo);
+ // `sub` is a nested git repository.
+ let _sub = git_init(&work_dir.join("sub"));
+
+ let tree = Worktree::local(
+ root.path(),
+ true,
+ Arc::new(RealFs::default()),
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ tree.flush_fs_events(cx).await;
+ cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
+ .await;
+ cx.executor().run_until_parked();
+
+ tree.read_with(cx, |tree, _cx| {
+ let snapshot = tree.snapshot();
+ let repo = snapshot.repositories().iter().next().unwrap();
+ let entries = repo.status().collect::<Vec<_>>();
+
+ // `sub` doesn't appear in our computed statuses.
+ assert_eq!(entries.len(), 1);
+ assert_eq!(entries[0].repo_path.as_ref(), Path::new("a.txt"));
+ // a.txt appears with a combined `DA` status.
+ assert_eq!(
+ entries[0].status,
+ TrackedStatus {
+ index_status: StatusCode::Deleted,
+ worktree_status: StatusCode::Added
+ }
+ .into()
+ );
+ });
+}
+
#[gpui::test]
async fn test_repository_subfolder_git_status(cx: &mut TestAppContext) {
init_test(cx);