1use std::iter;
19use std::path::Path;
20use std::path::PathBuf;
21
22use thiserror::Error;
23
24use crate::file_util;
25use crate::merge::Diff;
26use crate::repo_path::RelativePathParseError;
27use crate::repo_path::RepoPath;
28use crate::repo_path::RepoPathBuf;
29
30#[derive(Clone, Debug, Eq, Error, PartialEq)]
32#[error(r#"Path "{input}" is not in the repo "{base}""#)]
33pub struct FsPathParseError {
34 pub base: Box<Path>,
36 pub input: Box<Path>,
38 pub source: RelativePathParseError,
40}
41
42#[derive(Debug, Error)]
44pub enum UiPathParseError {
45 #[error(transparent)]
47 Fs(FsPathParseError),
48}
49
50#[derive(Debug, Clone)]
53pub enum RepoPathUiConverter {
54 Fs {
60 cwd: PathBuf,
62 base: PathBuf,
64 },
65 }
68
69impl RepoPathUiConverter {
70 pub fn format_file_path(&self, file: &RepoPath) -> String {
72 match self {
73 Self::Fs { cwd, base } => {
74 file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
75 .display()
76 .to_string()
77 }
78 }
79 }
80
81 pub fn format_copied_path(&self, paths: Diff<&RepoPath>) -> String {
87 match self {
88 Self::Fs { .. } => {
89 let paths = paths.map(|path| self.format_file_path(path));
90 collapse_copied_path(paths.as_deref(), std::path::MAIN_SEPARATOR)
91 }
92 }
93 }
94
95 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
100 match self {
101 Self::Fs { cwd, base } => parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs),
102 }
103 }
104}
105
106fn collapse_copied_path(paths: Diff<&str>, separator: char) -> String {
107 let components = paths.map(|path| path.split_inclusive(separator));
110 let prefix_len: usize = iter::zip(components.before, components.after)
111 .take_while(|(before, after)| before == after)
112 .map(|(_, after)| after.len())
113 .sum();
114 if paths.before.len() == prefix_len && paths.after.len() == prefix_len {
115 return paths.after.to_owned();
116 }
117
118 let components = paths.map(|path| {
122 let mut remainder = &path[prefix_len.saturating_sub(1)..];
123 iter::from_fn(move || {
124 let pos = remainder.rfind(separator)?;
125 let (prefix, last) = remainder.split_at(pos);
126 remainder = prefix;
127 Some(last)
128 })
129 });
130 let suffix_len: usize = iter::zip(components.before, components.after)
131 .take_while(|(before, after)| before == after)
132 .map(|(_, after)| after.len())
133 .sum();
134
135 let middle = paths.map(|path| path.get(prefix_len..path.len() - suffix_len).unwrap_or(""));
139
140 let mut collapsed = String::new();
141 collapsed.push_str(&paths.after[..prefix_len]);
142 collapsed.push('{');
143 collapsed.push_str(middle.before);
144 collapsed.push_str(" => ");
145 collapsed.push_str(middle.after);
146 collapsed.push('}');
147 collapsed.push_str(&paths.after[paths.after.len() - suffix_len..]);
148 collapsed
149}
150
151pub fn parse_fs_path(
157 cwd: &Path,
158 base: &Path,
159 input: impl AsRef<Path>,
160) -> Result<RepoPathBuf, FsPathParseError> {
161 let input = input.as_ref();
162 let abs_input_path = file_util::normalize_path(&cwd.join(input));
163 let repo_relative_path = file_util::relative_path(base, &abs_input_path);
164 RepoPathBuf::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError {
165 base: file_util::relative_path(cwd, base).into(),
166 input: input.into(),
167 source,
168 })
169}
170
171#[cfg(test)]
172mod tests {
173 use assert_matches::assert_matches;
174
175 use super::*;
176 use crate::tests::new_temp_dir;
177
178 fn repo_path(value: &str) -> &RepoPath {
179 RepoPath::from_internal_string(value).unwrap()
180 }
181
182 #[test]
183 fn test_format_copied_path() {
184 let ui = RepoPathUiConverter::Fs {
185 cwd: PathBuf::from("."),
186 base: PathBuf::from("."),
187 };
188
189 let format = |before, after| {
190 ui.format_copied_path(Diff::new(repo_path(before), repo_path(after)))
191 .replace('\\', "/")
192 };
193
194 assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
195 assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
196 assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
197 assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
198 assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
199 assert_eq!(
200 format("one/two", "one/two/three/one/two"),
201 "one/{ => two/three/one}/two"
202 );
203
204 assert_eq!(format("two/three", "four/three"), "{two => four}/three");
205 assert_eq!(
206 format("one/two/three", "one/four/three"),
207 "one/{two => four}/three"
208 );
209 assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
210 assert_eq!(format("one/two", "one/four"), "one/{two => four}");
211 assert_eq!(format("two", "four"), "{two => four}");
212 assert_eq!(format("file1", "file2"), "{file1 => file2}");
213 assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
214 assert_eq!(
215 format("x/something/something/2to1.txt", "x/something/2to1.txt"),
216 "x/something/{something => }/2to1.txt"
217 );
218 assert_eq!(
219 format("x/something/1to2.txt", "x/something/something/1to2.txt"),
220 "x/something/{ => something}/1to2.txt"
221 );
222 }
223
224 #[test]
225 fn parse_fs_path_wc_in_cwd() {
226 let temp_dir = new_temp_dir();
227 let cwd_path = temp_dir.path().join("repo");
228 let wc_path = &cwd_path;
229
230 assert_eq!(
231 parse_fs_path(&cwd_path, wc_path, "").as_deref(),
232 Ok(RepoPath::root())
233 );
234 assert_eq!(
235 parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
236 Ok(RepoPath::root())
237 );
238 assert_eq!(
239 parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
240 Ok(repo_path("file"))
241 );
242 assert_eq!(
244 parse_fs_path(
245 &cwd_path,
246 wc_path,
247 format!("dir{}file", std::path::MAIN_SEPARATOR)
248 )
249 .as_deref(),
250 Ok(repo_path("dir/file"))
251 );
252 assert_eq!(
253 parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
254 Ok(repo_path("dir/file"))
255 );
256 assert_matches!(
257 parse_fs_path(&cwd_path, wc_path, ".."),
258 Err(FsPathParseError {
259 source: RelativePathParseError::InvalidComponent { .. },
260 ..
261 })
262 );
263 assert_eq!(
264 parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
265 Ok(RepoPath::root())
266 );
267 assert_eq!(
268 parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
269 Ok(repo_path("file"))
270 );
271 assert_eq!(
273 parse_fs_path(
274 &cwd_path,
275 &cwd_path,
276 cwd_path.join("../repo").to_str().unwrap()
277 )
278 .as_deref(),
279 Ok(RepoPath::root())
280 );
281 }
282
283 #[test]
284 fn parse_fs_path_wc_in_cwd_parent() {
285 let temp_dir = new_temp_dir();
286 let cwd_path = temp_dir.path().join("dir");
287 let wc_path = cwd_path.parent().unwrap().to_path_buf();
288
289 assert_eq!(
290 parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
291 Ok(repo_path("dir"))
292 );
293 assert_eq!(
294 parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
295 Ok(repo_path("dir"))
296 );
297 assert_eq!(
298 parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
299 Ok(repo_path("dir/file"))
300 );
301 assert_eq!(
302 parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
303 Ok(repo_path("dir/subdir/file"))
304 );
305 assert_eq!(
306 parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
307 Ok(RepoPath::root())
308 );
309 assert_matches!(
310 parse_fs_path(&cwd_path, &wc_path, "../.."),
311 Err(FsPathParseError {
312 source: RelativePathParseError::InvalidComponent { .. },
313 ..
314 })
315 );
316 assert_eq!(
317 parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
318 Ok(repo_path("other-dir/file"))
319 );
320 }
321
322 #[test]
323 fn parse_fs_path_wc_in_cwd_child() {
324 let temp_dir = new_temp_dir();
325 let cwd_path = temp_dir.path().join("cwd");
326 let wc_path = cwd_path.join("repo");
327
328 assert_matches!(
329 parse_fs_path(&cwd_path, &wc_path, ""),
330 Err(FsPathParseError {
331 source: RelativePathParseError::InvalidComponent { .. },
332 ..
333 })
334 );
335 assert_matches!(
336 parse_fs_path(&cwd_path, &wc_path, "not-repo"),
337 Err(FsPathParseError {
338 source: RelativePathParseError::InvalidComponent { .. },
339 ..
340 })
341 );
342 assert_eq!(
343 parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
344 Ok(RepoPath::root())
345 );
346 assert_eq!(
347 parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
348 Ok(repo_path("file"))
349 );
350 assert_eq!(
351 parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
352 Ok(repo_path("dir/file"))
353 );
354 }
355}