Show type-changed files in commit diffs (#60422)

# Objective

Zed ignores files marked by Git as type-changed (T), causing commits
containing only these changes to show 0 Changed Files.

For example, commit d7cc949e61 changes
crates/eval_utils/LICENSE-GPL from a regular file to a symlink, but Zed
displays no changes.

  ## Solution

Handle TypeChanged files like modified files by loading both their old
and new contents.

  ## Testing

  - Added parser coverage for the T status.
  - Added a repository test for a regular-file-to-symlink change.
  - Ran cargo test -p git.
  - Ran ./script/clippy -p git.
  - Manually verified the example commit on macOS.

  ## Self-Review Checklist:
  - [x] I've reviewed my own diff for quality, security, and reliability
  - [x] Unsafe blocks (if any) have justifying comments
- [x] The content adheres to Zed's UI standards
([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
and [icon](https://github.com/zed-
  industries/zed/blob/main/crates/icons/README.md) guidelines)
  - [x] Tests cover the new/changed behavior
  - [x] Performance impact has been considered and is acceptable

  ## Showcase

Before:
<img width="200" alt="image"
src="https://github.com/user-attachments/assets/277c5938-5c2c-47b7-902d-9061a14c062a"
/>

After:
<img width="200" alt="image"
src="https://github.com/user-attachments/assets/f99d0005-0712-4843-814e-d73b11f64782"
/>

  ———

  Release Notes:

- Fixed type-changed files not appearing in Git Graph and commit views.
This commit is contained in:
Eagl61 2026-07-06 05:15:43 +02:00 committed by GitHub
parent b4d9194fce
commit 04de6dab7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 60 additions and 2 deletions

View file

@ -109,6 +109,7 @@ pub fn parse_git_diff_name_status(content: &str) -> impl Iterator<Item = (&str,
let path = parts.next()?;
let status = match status_str {
"M" => StatusCode::Modified,
"T" => StatusCode::TypeChanged,
"A" => StatusCode::Added,
"D" => StatusCode::Deleted,
_ => continue,
@ -127,6 +128,7 @@ mod tests {
fn test_parse_git_diff_name_status() {
let input = concat!(
"M\x00Cargo.lock\x00",
"T\x00LICENSE-GPL\x00",
"M\x00crates/project/Cargo.toml\x00",
"M\x00crates/project/src/buffer_store.rs\x00",
"D\x00crates/project/src/git.rs\x00",
@ -142,6 +144,7 @@ mod tests {
output,
&[
("Cargo.lock", StatusCode::Modified),
("LICENSE-GPL", StatusCode::TypeChanged),
("crates/project/Cargo.toml", StatusCode::Modified),
("crates/project/src/buffer_store.rs", StatusCode::Modified),
("crates/project/src/git.rs", StatusCode::Deleted),

View file

@ -1445,7 +1445,7 @@ impl GitRepository for RealGitRepository {
};
match status_code {
StatusCode::Modified => {
StatusCode::Modified | StatusCode::TypeChanged => {
stdin.write_all(commit.as_bytes()).await?;
stdin.write_all(b":").await?;
stdin.write_all(path.as_bytes()).await?;
@ -1491,7 +1491,7 @@ impl GitRepository for RealGitRepository {
};
match status_code {
StatusCode::Modified => {
StatusCode::Modified | StatusCode::TypeChanged => {
info_line.clear();
stdout.read_line(&mut info_line).await?;
let len = info_line.trim_end().parse().with_context(|| {
@ -4031,6 +4031,61 @@ mod tests {
);
}
#[gpui::test]
async fn test_load_commit_with_type_changed_file(cx: &mut TestAppContext) {
disable_git_global_config();
cx.executor().allow_parking();
let repo_dir = tempfile::tempdir().expect("failed to create temporary repository");
git_init_repo(repo_dir.path());
fs::write(repo_dir.path().join("file.txt"), "regular contents\n")
.expect("failed to write regular file");
git_command(repo_dir.path(), ["add", "file.txt"]);
git_command(repo_dir.path(), ["commit", "-m", "initial"]);
let repository = RealGitRepository::new(
&repo_dir.path().join(".git"),
None,
Some("git".into()),
cx.executor(),
)
.expect("failed to open repository");
fs::write(repo_dir.path().join("file.txt"), "target")
.expect("failed to write symlink target");
let symlink_blob = repository
.git_binary()
.run(&["hash-object", "-w", "file.txt"])
.await
.expect("failed to write symlink blob");
git_command(
repo_dir.path(),
[
OsString::from("update-index"),
OsString::from("--cacheinfo"),
OsString::from("120000"),
OsString::from(symlink_blob),
OsString::from("file.txt"),
],
);
git_command(repo_dir.path(), ["commit", "-m", "type change"]);
let commit_diff = repository
.load_commit("HEAD".to_string(), cx.to_async())
.await
.expect("failed to load type-changed commit");
assert_eq!(commit_diff.files.len(), 1);
let file = commit_diff
.files
.first()
.expect("type-changed file should be present");
assert_eq!(file.path.as_unix_str(), "file.txt");
assert_eq!(file.old_text.as_deref(), Some("regular contents\n"));
assert_eq!(file.new_text.as_deref(), Some("target"));
assert_eq!(file.status(), CommitFileStatus::Modified);
}
#[gpui::test]
async fn test_check_access(cx: &mut TestAppContext) {
disable_git_global_config();