Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions crates/git/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
///
Expand Down Expand Up @@ -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(),
})
Expand Down
56 changes: 56 additions & 0 deletions crates/worktree/src/worktree_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down