Fix git features not working when a Windows host collaborates with a unix guest (#43515)

We were using `std::path::Path::strip_prefix` to determine which
repository an absolute path belongs to, which doesn't work when the
paths are Windows-style but the code is running on unix. Replace it with
a platform-agnostic implementation of `strip_prefix`.

Release Notes:

- Fixed git features not working when a Windows host collaborates with a
unix guest
This commit is contained in:
Cole Miller
2025-11-26 11:56:34 -05:00
committed by GitHub
parent 57e1bb8106
commit 757c043171
15 changed files with 159 additions and 30 deletions

View File

@@ -1423,7 +1423,7 @@ mod tests {
rel_path("b/eight.txt"),
];
let slash = PathStyle::local().separator();
let slash = PathStyle::local().primary_separator();
let mut opened_editors = Vec::new();
for path in paths {

View File

@@ -3989,7 +3989,7 @@ impl AcpThreadView {
let file = buffer.read(cx).file()?;
let path = file.path();
let path_style = file.path_style(cx);
let separator = file.path_style(cx).separator();
let separator = file.path_style(cx).primary_separator();
let file_path = path.parent().and_then(|parent| {
if parent.is_empty() {

View File

@@ -1060,7 +1060,7 @@ impl FileFinderDelegate {
(
filename.to_string(),
Vec::new(),
prefix.display(path_style).to_string() + path_style.separator(),
prefix.display(path_style).to_string() + path_style.primary_separator(),
Vec::new(),
)
} else {
@@ -1071,7 +1071,7 @@ impl FileFinderDelegate {
.map_or(String::new(), |f| f.to_string_lossy().into_owned()),
Vec::new(),
entry_path.absolute.parent().map_or(String::new(), |path| {
path.to_string_lossy().into_owned() + path_style.separator()
path.to_string_lossy().into_owned() + path_style.primary_separator()
}),
Vec::new(),
)

View File

@@ -1598,7 +1598,7 @@ async fn test_history_match_positions(cx: &mut gpui::TestAppContext) {
assert_eq!(file_label.highlight_indices(), &[0, 1, 2]);
assert_eq!(
path_label.text(),
format!("test{}", PathStyle::local().separator())
format!("test{}", PathStyle::local().primary_separator())
);
assert_eq!(path_label.highlight_indices(), &[] as &[usize]);
});

View File

@@ -559,7 +559,7 @@ impl PickerDelegate for OpenPathDelegate {
parent_path,
candidate.path.string,
if candidate.is_dir {
path_style.separator()
path_style.primary_separator()
} else {
""
}
@@ -569,7 +569,7 @@ impl PickerDelegate for OpenPathDelegate {
parent_path,
candidate.path.string,
if candidate.is_dir {
path_style.separator()
path_style.primary_separator()
} else {
""
}
@@ -826,7 +826,13 @@ impl PickerDelegate for OpenPathDelegate {
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
Arc::from(format!("[directory{}]filename.ext", self.path_style.separator()).as_str())
Arc::from(
format!(
"[directory{}]filename.ext",
self.path_style.primary_separator()
)
.as_str(),
)
}
fn separators_after_indices(&self) -> Vec<usize> {

View File

@@ -107,7 +107,7 @@ pub fn match_fixed_path_set(
.display(path_style)
.chars()
.collect::<Vec<_>>();
path_prefix_chars.extend(path_style.separator().chars());
path_prefix_chars.extend(path_style.primary_separator().chars());
let lowercase_pfx = path_prefix_chars
.iter()
.map(|c| c.to_ascii_lowercase())

View File

@@ -4351,8 +4351,11 @@ impl GitPanel {
.when(strikethrough, Label::strikethrough),
),
(true, false) => this.child(
self.entry_label(format!("{dir}{}", path_style.separator()), path_color)
.when(strikethrough, Label::strikethrough),
self.entry_label(
format!("{dir}{}", path_style.primary_separator()),
path_color,
)
.when(strikethrough, Label::strikethrough),
),
_ => this,
}

View File

@@ -3222,10 +3222,8 @@ impl RepositorySnapshot {
abs_path: &Path,
path_style: PathStyle,
) -> Option<RepoPath> {
abs_path
.strip_prefix(&work_directory_abs_path)
.ok()
.and_then(|path| RepoPath::from_std_path(path, path_style).ok())
let rel_path = path_style.strip_prefix(abs_path, work_directory_abs_path)?;
Some(RepoPath::from_rel_path(&rel_path))
}
pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool {

View File

@@ -927,7 +927,7 @@ impl DirectoryLister {
.map(|worktree| worktree.read(cx).abs_path().to_string_lossy().into_owned())
.or_else(|| std::env::home_dir().map(|dir| dir.to_string_lossy().into_owned()))
.map(|mut s| {
s.push_str(path_style.separator());
s.push_str(path_style.primary_separator());
s
})
.unwrap_or_else(|| {

View File

@@ -4837,7 +4837,7 @@ impl ProjectPanel {
.collect::<Vec<_>>();
let active_index = folded_ancestors.active_index();
let components_len = components.len();
let delimiter = SharedString::new(path_style.separator());
let delimiter = SharedString::new(path_style.primary_separator());
for (index, component) in components.iter().enumerate() {
if index != 0 {
let delimiter_target_index = index - 1;

View File

@@ -2192,7 +2192,7 @@ impl SettingsWindow {
format!(
"{}{}{}",
directory_name,
path_style.separator(),
path_style.primary_separator(),
path.display(path_style)
)
}

View File

@@ -876,7 +876,7 @@ impl ToolchainSelectorDelegate {
.strip_prefix(&worktree_root)
.ok()
.and_then(|suffix| suffix.to_str())
.map(|suffix| format!(".{}{suffix}", path_style.separator()).into())
.map(|suffix| format!(".{}{suffix}", path_style.primary_separator()).into())
.unwrap_or(path)
}
}

View File

@@ -3,6 +3,7 @@ use globset::{Glob, GlobSet, GlobSetBuilder};
use itertools::Itertools;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::cmp::Ordering;
use std::error::Error;
use std::fmt::{Display, Formatter};
@@ -331,13 +332,20 @@ impl PathStyle {
}
#[inline]
pub fn separator(&self) -> &'static str {
pub fn primary_separator(&self) -> &'static str {
match self {
PathStyle::Posix => "/",
PathStyle::Windows => "\\",
}
}
pub fn separators(&self) -> &'static [&'static str] {
match self {
PathStyle::Posix => &["/"],
PathStyle::Windows => &["\\", "/"],
}
}
pub fn is_windows(&self) -> bool {
*self == PathStyle::Windows
}
@@ -353,25 +361,54 @@ impl PathStyle {
} else {
Some(format!(
"{left}{}{right}",
if left.ends_with(self.separator()) {
if left.ends_with(self.primary_separator()) {
""
} else {
self.separator()
self.primary_separator()
}
))
}
}
pub fn split(self, path_like: &str) -> (Option<&str>, &str) {
let Some(pos) = path_like.rfind(self.separator()) else {
let Some(pos) = path_like.rfind(self.primary_separator()) else {
return (None, path_like);
};
let filename_start = pos + self.separator().len();
let filename_start = pos + self.primary_separator().len();
(
Some(&path_like[..filename_start]),
&path_like[filename_start..],
)
}
pub fn strip_prefix<'a>(
&self,
child: &'a Path,
parent: &'a Path,
) -> Option<std::borrow::Cow<'a, RelPath>> {
let parent = parent.to_str()?;
if parent.is_empty() {
return RelPath::new(child, *self).ok();
}
let parent = self
.separators()
.iter()
.find_map(|sep| parent.strip_suffix(sep))
.unwrap_or(parent);
let child = child.to_str()?;
let stripped = child.strip_prefix(parent)?;
if let Some(relative) = self
.separators()
.iter()
.find_map(|sep| stripped.strip_prefix(sep))
{
RelPath::new(relative.as_ref(), *self).ok()
} else if stripped.is_empty() {
Some(Cow::Borrowed(RelPath::empty()))
} else {
None
}
}
}
#[derive(Debug, Clone)]
@@ -788,7 +825,7 @@ impl PathMatcher {
fn check_with_end_separator(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
let separator = self.path_style.separator();
let separator = self.path_style.primary_separator();
if path_str.ends_with(separator) {
false
} else {
@@ -1311,6 +1348,8 @@ impl WslPath {
#[cfg(test)]
mod tests {
use crate::rel_path::rel_path;
use super::*;
use util_macros::perf;
@@ -2480,6 +2519,89 @@ mod tests {
assert_eq!(strip_path_suffix(base, suffix), None);
}
#[test]
fn test_strip_prefix() {
let expected = [
(
PathStyle::Posix,
"/a/b/c",
"/a/b",
Some(rel_path("c").into_arc()),
),
(
PathStyle::Posix,
"/a/b/c",
"/a/b/",
Some(rel_path("c").into_arc()),
),
(
PathStyle::Posix,
"/a/b/c",
"/",
Some(rel_path("a/b/c").into_arc()),
),
(PathStyle::Posix, "/a/b/c", "", None),
(PathStyle::Posix, "/a/b//c", "/a/b/", None),
(PathStyle::Posix, "/a/bc", "/a/b", None),
(
PathStyle::Posix,
"/a/b/c",
"/a/b/c",
Some(rel_path("").into_arc()),
),
(
PathStyle::Windows,
"C:\\a\\b\\c",
"C:\\a\\b",
Some(rel_path("c").into_arc()),
),
(
PathStyle::Windows,
"C:\\a\\b\\c",
"C:\\a\\b\\",
Some(rel_path("c").into_arc()),
),
(
PathStyle::Windows,
"C:\\a\\b\\c",
"C:\\",
Some(rel_path("a/b/c").into_arc()),
),
(PathStyle::Windows, "C:\\a\\b\\c", "", None),
(PathStyle::Windows, "C:\\a\\b\\\\c", "C:\\a\\b\\", None),
(PathStyle::Windows, "C:\\a\\bc", "C:\\a\\b", None),
(
PathStyle::Windows,
"C:\\a\\b/c",
"C:\\a\\b",
Some(rel_path("c").into_arc()),
),
(
PathStyle::Windows,
"C:\\a\\b/c",
"C:\\a\\b\\",
Some(rel_path("c").into_arc()),
),
(
PathStyle::Windows,
"C:\\a\\b/c",
"C:\\a\\b/",
Some(rel_path("c").into_arc()),
),
];
let actual = expected.clone().map(|(style, child, parent, _)| {
(
style,
child,
parent,
style
.strip_prefix(child.as_ref(), parent.as_ref())
.map(|rel_path| rel_path.into_arc()),
)
});
pretty_assertions::assert_eq!(actual, expected);
}
#[cfg(target_os = "windows")]
#[test]
fn test_wsl_path() {

View File

@@ -965,7 +965,7 @@ impl VimCommand {
}
};
let rel_path = if args.ends_with(PathStyle::local().separator()) {
let rel_path = if args.ends_with(PathStyle::local().primary_separator()) {
rel_path
} else {
rel_path
@@ -998,7 +998,7 @@ impl VimCommand {
.display(PathStyle::local())
.to_string();
if dir.is_dir {
path_string.push_str(PathStyle::local().separator());
path_string.push_str(PathStyle::local().primary_separator());
}
path_string
})

View File

@@ -999,7 +999,7 @@ impl Worktree {
};
if worktree_relative_path.components().next().is_some() {
full_path_string.push_str(self.path_style.separator());
full_path_string.push_str(self.path_style.primary_separator());
full_path_string.push_str(&worktree_relative_path.display(self.path_style));
}
@@ -2108,8 +2108,8 @@ impl Snapshot {
if path.file_name().is_some() {
let mut abs_path = self.abs_path.to_string();
for component in path.components() {
if !abs_path.ends_with(self.path_style.separator()) {
abs_path.push_str(self.path_style.separator());
if !abs_path.ends_with(self.path_style.primary_separator()) {
abs_path.push_str(self.path_style.primary_separator());
}
abs_path.push_str(component);
}