Fix track file renames in git panel (#42352)

Closes #30549

Release Notes:

- Fixed: Git renames now properly show as renamed files in the git panel
instead of appearing as deleted + untracked files
<img width="351" height="132" alt="Screenshot 2025-11-10 at 17 39 44"
src="https://github.com/user-attachments/assets/80e9c286-1abd-4498-a7d5-bd21633e6597"
/>
<img width="500" height="95" alt="Screenshot 2025-11-10 at 17 39 55"
src="https://github.com/user-attachments/assets/e4c59796-df3a-4d12-96f4-e6706b13a32f"
/>
This commit is contained in:
ozzy
2025-11-17 13:25:51 -06:00
committed by GitHub
parent 57e3bcfcf8
commit b0a7defd09
9 changed files with 151 additions and 45 deletions
+1 -1
View File
@@ -2045,7 +2045,7 @@ fn git_status_args(path_prefixes: &[RepoPath]) -> Vec<OsString> {
OsString::from("status"),
OsString::from("--porcelain=v1"),
OsString::from("--untracked-files=all"),
OsString::from("--no-renames"),
OsString::from("--find-renames"),
OsString::from("-z"),
];
args.extend(
+75 -20
View File
@@ -203,6 +203,14 @@ impl FileStatus {
matches!(self, FileStatus::Untracked)
}
pub fn is_renamed(self) -> bool {
let FileStatus::Tracked(tracked) = self else {
return false;
};
tracked.index_status == StatusCode::Renamed
|| tracked.worktree_status == StatusCode::Renamed
}
pub fn summary(self) -> GitSummary {
match self {
FileStatus::Ignored => GitSummary::UNCHANGED,
@@ -430,34 +438,79 @@ impl std::ops::Sub for GitSummary {
#[derive(Clone, Debug)]
pub struct GitStatus {
pub entries: Arc<[(RepoPath, FileStatus)]>,
pub renamed_paths: HashMap<RepoPath, RepoPath>,
}
impl FromStr for GitStatus {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let mut entries = s
.split('\0')
.filter_map(|entry| {
let sep = entry.get(2..3)?;
if sep != " " {
return None;
let mut parts = s.split('\0').peekable();
let mut entries = Vec::new();
let mut renamed_paths = HashMap::default();
while let Some(entry) = parts.next() {
if entry.is_empty() {
continue;
}
if !matches!(entry.get(2..3), Some(" ")) {
continue;
}
let path_or_old_path = &entry[3..];
if path_or_old_path.ends_with('/') {
continue;
}
let status = match entry.as_bytes()[0..2].try_into() {
Ok(bytes) => match FileStatus::from_bytes(bytes).log_err() {
Some(s) => s,
None => continue,
},
Err(_) => continue,
};
let is_rename = matches!(
status,
FileStatus::Tracked(TrackedStatus {
index_status: StatusCode::Renamed | StatusCode::Copied,
..
}) | FileStatus::Tracked(TrackedStatus {
worktree_status: StatusCode::Renamed | StatusCode::Copied,
..
})
);
let (old_path_str, new_path_str) = if is_rename {
let new_path = match parts.next() {
Some(new_path) if !new_path.is_empty() => new_path,
_ => continue,
};
let path = &entry[3..];
// The git status output includes untracked directories as well as untracked files.
// We do our own processing to compute the "summary" status of each directory,
// so just skip any directories in the output, since they'll otherwise interfere
// with our handling of nested repositories.
if path.ends_with('/') {
return None;
(path_or_old_path, new_path)
} else {
(path_or_old_path, path_or_old_path)
};
if new_path_str.ends_with('/') {
continue;
}
let new_path = match RelPath::unix(new_path_str).log_err() {
Some(p) => RepoPath::from_rel_path(p),
None => continue,
};
if is_rename {
if let Some(old_path_rel) = RelPath::unix(old_path_str).log_err() {
let old_path_repo = RepoPath::from_rel_path(old_path_rel);
renamed_paths.insert(new_path.clone(), old_path_repo);
}
let status = entry.as_bytes()[0..2].try_into().unwrap();
let status = FileStatus::from_bytes(status).log_err()?;
// git-status outputs `/`-delimited repo paths, even on Windows.
let path = RepoPath::from_rel_path(RelPath::unix(path).log_err()?);
Some((path, status))
})
.collect::<Vec<_>>();
}
entries.push((new_path, status));
}
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)
@@ -481,6 +534,7 @@ impl FromStr for GitStatus {
});
Ok(Self {
entries: entries.into(),
renamed_paths,
})
}
}
@@ -489,6 +543,7 @@ impl Default for GitStatus {
fn default() -> Self {
Self {
entries: Arc::new([]),
renamed_paths: HashMap::default(),
}
}
}