Detailed changes
@@ -1005,6 +1005,7 @@ impl Database {
is_last_update: true,
merge_message: db_repository_entry.merge_message,
stash_entries: Vec::new(),
+ renamed_paths: Default::default(),
});
}
}
@@ -796,6 +796,7 @@ impl Database {
is_last_update: true,
merge_message: db_repository.merge_message,
stash_entries: Vec::new(),
+ renamed_paths: Default::default(),
});
}
}
@@ -359,6 +359,7 @@ impl GitRepository for FakeGitRepository {
entries.sort_by(|a, b| a.0.cmp(&b.0));
anyhow::Ok(GitStatus {
entries: entries.into(),
+ renamed_paths: HashMap::default(),
})
});
Task::ready(match result {
@@ -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(
@@ -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(),
}
}
}
@@ -3957,6 +3957,20 @@ impl GitPanel {
let path_style = self.project.read(cx).path_style(cx);
let display_name = entry.display_name(path_style);
+ let active_repo = self
+ .project
+ .read(cx)
+ .active_repository(cx)
+ .expect("active repository must be set");
+ let repo = active_repo.read(cx);
+ let repo_snapshot = repo.snapshot();
+
+ let old_path = if entry.status.is_renamed() {
+ repo_snapshot.renamed_paths.get(&entry.repo_path)
+ } else {
+ None
+ };
+
let selected = self.selected_entry == Some(ix);
let marked = self.marked_entries.contains(&ix);
let status_style = GitPanelSettings::get_global(cx).status_style;
@@ -3965,15 +3979,16 @@ impl GitPanel {
let has_conflict = status.is_conflicted();
let is_modified = status.is_modified();
let is_deleted = status.is_deleted();
+ let is_renamed = status.is_renamed();
let label_color = if status_style == StatusStyle::LabelColor {
if has_conflict {
Color::VersionControlConflict
- } else if is_modified {
- Color::VersionControlModified
} else if is_deleted {
// We don't want a bunch of red labels in the list
Color::Disabled
+ } else if is_renamed || is_modified {
+ Color::VersionControlModified
} else {
Color::VersionControlAdded
}
@@ -3993,12 +4008,6 @@ impl GitPanel {
let checkbox_id: ElementId =
ElementId::Name(format!("entry_{}_{}_checkbox", display_name, ix).into());
- let active_repo = self
- .project
- .read(cx)
- .active_repository(cx)
- .expect("active repository must be set");
- let repo = active_repo.read(cx);
// Checking for current staged/unstaged file status is a chained operation:
// 1. first, we check for any pending operation recorded in repository
// 2. if there are no pending ops either running or finished, we then ask the repository
@@ -4153,23 +4162,32 @@ impl GitPanel {
.items_center()
.flex_1()
// .overflow_hidden()
- .when_some(entry.parent_dir(path_style), |this, parent| {
- if !parent.is_empty() {
- this.child(
- self.entry_label(
- format!("{parent}{}", path_style.separator()),
- path_color,
+ .when_some(old_path.as_ref(), |this, old_path| {
+ let new_display = old_path.display(path_style).to_string();
+ let old_display = entry.repo_path.display(path_style).to_string();
+ this.child(self.entry_label(old_display, Color::Muted).strikethrough())
+ .child(self.entry_label(" → ", Color::Muted))
+ .child(self.entry_label(new_display, label_color))
+ })
+ .when(old_path.is_none(), |this| {
+ this.when_some(entry.parent_dir(path_style), |this, parent| {
+ if !parent.is_empty() {
+ this.child(
+ self.entry_label(
+ format!("{parent}{}", path_style.separator()),
+ path_color,
+ )
+ .when(status.is_deleted(), |this| this.strikethrough()),
)
+ } else {
+ this
+ }
+ })
+ .child(
+ self.entry_label(display_name, label_color)
.when(status.is_deleted(), |this| this.strikethrough()),
- )
- } else {
- this
- }
- })
- .child(
- self.entry_label(display_name, label_color)
- .when(status.is_deleted(), |this| this.strikethrough()),
- ),
+ )
+ }),
)
.into_any_element()
}
@@ -708,6 +708,11 @@ impl RenderOnce for GitStatusIcon {
IconName::SquareMinus,
cx.theme().colors().version_control_deleted,
)
+ } else if status.is_renamed() {
+ (
+ IconName::ArrowRight,
+ cx.theme().colors().version_control_modified,
+ )
} else if status.is_modified() {
(
IconName::SquareDot,
@@ -256,6 +256,7 @@ pub struct RepositorySnapshot {
pub id: RepositoryId,
pub statuses_by_path: SumTree<StatusEntry>,
pub pending_ops_by_path: SumTree<PendingOps>,
+ pub renamed_paths: HashMap<RepoPath, RepoPath>,
pub work_directory_abs_path: Arc<Path>,
pub path_style: PathStyle,
pub branch: Option<Branch>,
@@ -3063,6 +3064,7 @@ impl RepositorySnapshot {
id,
statuses_by_path: Default::default(),
pending_ops_by_path: Default::default(),
+ renamed_paths: HashMap::default(),
work_directory_abs_path,
branch: None,
head_commit: None,
@@ -3104,6 +3106,11 @@ impl RepositorySnapshot {
.iter()
.map(stash_to_proto)
.collect(),
+ renamed_paths: self
+ .renamed_paths
+ .iter()
+ .map(|(new_path, old_path)| (new_path.to_proto(), old_path.to_proto()))
+ .collect(),
}
}
@@ -3173,6 +3180,11 @@ impl RepositorySnapshot {
.iter()
.map(stash_to_proto)
.collect(),
+ renamed_paths: self
+ .renamed_paths
+ .iter()
+ .map(|(new_path, old_path)| (new_path.to_proto(), old_path.to_proto()))
+ .collect(),
}
}
@@ -4968,6 +4980,17 @@ impl Repository {
}
self.snapshot.stash_entries = new_stash_entries;
+ self.snapshot.renamed_paths = update
+ .renamed_paths
+ .into_iter()
+ .filter_map(|(new_path_str, old_path_str)| {
+ Some((
+ RepoPath::from_proto(&new_path_str).log_err()?,
+ RepoPath::from_proto(&old_path_str).log_err()?,
+ ))
+ })
+ .collect();
+
let edits = update
.removed_statuses
.into_iter()
@@ -5743,6 +5766,7 @@ async fn compute_snapshot(
id,
statuses_by_path,
pending_ops_by_path,
+ renamed_paths: statuses.renamed_paths,
work_directory_abs_path,
path_style: prev_snapshot.path_style,
scan_id: prev_snapshot.scan_id + 1,
@@ -124,6 +124,7 @@ message UpdateRepository {
optional GitCommitDetails head_commit_details = 11;
optional string merge_message = 12;
repeated StashEntry stash_entries = 13;
+ map<string, string> renamed_paths = 14;
}
message RemoveRepository {