From 04de6dab7c890d815e316f2f9554f9eeeaf8ebb2 Mon Sep 17 00:00:00 2001
From: Eagl61 <17625015+Eagl61@users.noreply.github.com>
Date: Mon, 6 Jul 2026 05:15:43 +0200
Subject: [PATCH] Show type-changed files in commit diffs (#60422)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# 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 d7cc949e61351a6028f933abbaea39f67af9dfb2 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:
After:
———
Release Notes:
- Fixed type-changed files not appearing in Git Graph and commit views.
---
crates/git/src/commit.rs | 3 ++
crates/git/src/repository.rs | 59 ++++++++++++++++++++++++++++++++++--
2 files changed, 60 insertions(+), 2 deletions(-)
diff --git a/crates/git/src/commit.rs b/crates/git/src/commit.rs
index 50b62fa506b..326c741ad19 100644
--- a/crates/git/src/commit.rs
+++ b/crates/git/src/commit.rs
@@ -109,6 +109,7 @@ pub fn parse_git_diff_name_status(content: &str) -> impl Iterator- 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),
diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs
index f09c13f54ab..2951a84b090 100644
--- a/crates/git/src/repository.rs
+++ b/crates/git/src/repository.rs
@@ -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();