jj_lib/
repo_path.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![expect(missing_docs)]
16
17use std::borrow::Borrow;
18use std::cmp::Ordering;
19use std::fmt;
20use std::fmt::Debug;
21use std::fmt::Formatter;
22use std::iter::FusedIterator;
23use std::ops::Deref;
24use std::path::Component;
25use std::path::Path;
26use std::path::PathBuf;
27
28use itertools::Itertools as _;
29use ref_cast::RefCastCustom;
30use ref_cast::ref_cast_custom;
31use thiserror::Error;
32
33use crate::content_hash::ContentHash;
34use crate::file_util;
35
36/// Owned `RepoPath` component.
37#[derive(ContentHash, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub struct RepoPathComponentBuf {
39    // Don't add more fields. Eq, Hash, and Ord must be compatible with the
40    // borrowed RepoPathComponent type.
41    value: String,
42}
43
44impl RepoPathComponentBuf {
45    /// Wraps `value` as `RepoPathComponentBuf`.
46    ///
47    /// Returns an error if the input `value` is empty or contains path
48    /// separator.
49    pub fn new(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
50        let value: String = value.into();
51        if is_valid_repo_path_component_str(&value) {
52            Ok(Self { value })
53        } else {
54            Err(InvalidNewRepoPathError { value })
55        }
56    }
57}
58
59/// Borrowed `RepoPath` component.
60#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, RefCastCustom)]
61#[repr(transparent)]
62pub struct RepoPathComponent {
63    value: str,
64}
65
66impl RepoPathComponent {
67    /// Wraps `value` as `RepoPathComponent`.
68    ///
69    /// Returns an error if the input `value` is empty or contains path
70    /// separator.
71    pub fn new(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
72        if is_valid_repo_path_component_str(value) {
73            Ok(Self::new_unchecked(value))
74        } else {
75            Err(InvalidNewRepoPathError {
76                value: value.to_string(),
77            })
78        }
79    }
80
81    #[ref_cast_custom]
82    const fn new_unchecked(value: &str) -> &Self;
83
84    /// Returns the underlying string representation.
85    pub fn as_internal_str(&self) -> &str {
86        &self.value
87    }
88
89    /// Returns a normal filesystem entry name if this path component is valid
90    /// as a file/directory name.
91    pub fn to_fs_name(&self) -> Result<&str, InvalidRepoPathComponentError> {
92        let mut components = Path::new(&self.value).components().fuse();
93        match (components.next(), components.next()) {
94            // Trailing "." can be normalized by Path::components(), so compare
95            // component name. e.g. "foo\." (on Windows) should be rejected.
96            (Some(Component::Normal(name)), None) if name == &self.value => Ok(&self.value),
97            // e.g. ".", "..", "foo\bar" (on Windows)
98            _ => Err(InvalidRepoPathComponentError {
99                component: self.value.into(),
100            }),
101        }
102    }
103}
104
105impl Debug for RepoPathComponent {
106    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
107        write!(f, "{:?}", &self.value)
108    }
109}
110
111impl Debug for RepoPathComponentBuf {
112    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113        <RepoPathComponent as Debug>::fmt(self, f)
114    }
115}
116
117impl AsRef<Self> for RepoPathComponent {
118    fn as_ref(&self) -> &Self {
119        self
120    }
121}
122
123impl AsRef<RepoPathComponent> for RepoPathComponentBuf {
124    fn as_ref(&self) -> &RepoPathComponent {
125        self
126    }
127}
128
129impl Borrow<RepoPathComponent> for RepoPathComponentBuf {
130    fn borrow(&self) -> &RepoPathComponent {
131        self
132    }
133}
134
135impl Deref for RepoPathComponentBuf {
136    type Target = RepoPathComponent;
137
138    fn deref(&self) -> &Self::Target {
139        RepoPathComponent::new_unchecked(&self.value)
140    }
141}
142
143impl ToOwned for RepoPathComponent {
144    type Owned = RepoPathComponentBuf;
145
146    fn to_owned(&self) -> Self::Owned {
147        let value = self.value.to_owned();
148        RepoPathComponentBuf { value }
149    }
150
151    fn clone_into(&self, target: &mut Self::Owned) {
152        self.value.clone_into(&mut target.value);
153    }
154}
155
156/// Iterator over `RepoPath` components.
157#[derive(Clone, Debug)]
158pub struct RepoPathComponentsIter<'a> {
159    value: &'a str,
160}
161
162impl<'a> RepoPathComponentsIter<'a> {
163    /// Returns the remaining part as repository path.
164    pub fn as_path(&self) -> &'a RepoPath {
165        RepoPath::from_internal_string_unchecked(self.value)
166    }
167}
168
169impl<'a> Iterator for RepoPathComponentsIter<'a> {
170    type Item = &'a RepoPathComponent;
171
172    fn next(&mut self) -> Option<Self::Item> {
173        if self.value.is_empty() {
174            return None;
175        }
176        let (name, remainder) = self
177            .value
178            .split_once('/')
179            .unwrap_or_else(|| (self.value, &self.value[self.value.len()..]));
180        self.value = remainder;
181        Some(RepoPathComponent::new_unchecked(name))
182    }
183}
184
185impl DoubleEndedIterator for RepoPathComponentsIter<'_> {
186    fn next_back(&mut self) -> Option<Self::Item> {
187        if self.value.is_empty() {
188            return None;
189        }
190        let (remainder, name) = self
191            .value
192            .rsplit_once('/')
193            .unwrap_or_else(|| (&self.value[..0], self.value));
194        self.value = remainder;
195        Some(RepoPathComponent::new_unchecked(name))
196    }
197}
198
199impl FusedIterator for RepoPathComponentsIter<'_> {}
200
201/// Owned repository path.
202#[derive(ContentHash, Clone, Eq, Hash, PartialEq, serde::Serialize)]
203#[serde(transparent)]
204pub struct RepoPathBuf {
205    // Don't add more fields. Eq, Hash, and Ord must be compatible with the
206    // borrowed RepoPath type.
207    value: String,
208}
209
210/// Borrowed repository path.
211#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom, serde::Serialize)]
212#[repr(transparent)]
213#[serde(transparent)]
214pub struct RepoPath {
215    value: str,
216}
217
218impl Debug for RepoPath {
219    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
220        write!(f, "{:?}", &self.value)
221    }
222}
223
224impl Debug for RepoPathBuf {
225    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
226        <RepoPath as Debug>::fmt(self, f)
227    }
228}
229
230/// The `value` is not a valid repo path because it contains empty path
231/// component. For example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all
232/// invalid.
233#[derive(Clone, Debug, Eq, Error, PartialEq)]
234#[error(r#"Invalid repo path input "{value}""#)]
235pub struct InvalidNewRepoPathError {
236    value: String,
237}
238
239impl RepoPathBuf {
240    /// Creates owned repository path pointing to the root.
241    pub const fn root() -> Self {
242        Self {
243            value: String::new(),
244        }
245    }
246
247    /// Creates `RepoPathBuf` from valid string representation.
248    pub fn from_internal_string(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
249        let value: String = value.into();
250        if is_valid_repo_path_str(&value) {
251            Ok(Self { value })
252        } else {
253            Err(InvalidNewRepoPathError { value })
254        }
255    }
256
257    /// Converts repo-relative `Path` to `RepoPathBuf`.
258    ///
259    /// The input path should not contain redundant `.` or `..`.
260    pub fn from_relative_path(
261        relative_path: impl AsRef<Path>,
262    ) -> Result<Self, RelativePathParseError> {
263        let relative_path = relative_path.as_ref();
264        if relative_path == Path::new(".") {
265            return Ok(Self::root());
266        }
267
268        let mut components = relative_path
269            .components()
270            .map(|c| match c {
271                Component::Normal(name) => {
272                    name.to_str()
273                        .ok_or_else(|| RelativePathParseError::InvalidUtf8 {
274                            path: relative_path.into(),
275                        })
276                }
277                _ => Err(RelativePathParseError::InvalidComponent {
278                    component: c.as_os_str().to_string_lossy().into(),
279                    path: relative_path.into(),
280                }),
281            })
282            .fuse();
283        let mut value = String::with_capacity(relative_path.as_os_str().len());
284        if let Some(name) = components.next() {
285            value.push_str(name?);
286        }
287        for name in components {
288            value.push('/');
289            value.push_str(name?);
290        }
291        Ok(Self { value })
292    }
293
294    /// Parses an `input` path into a `RepoPathBuf` relative to `base`.
295    ///
296    /// The `cwd` and `base` paths are supposed to be absolute and normalized in
297    /// the same manner. The `input` path may be either relative to `cwd` or
298    /// absolute.
299    pub fn parse_fs_path(
300        cwd: &Path,
301        base: &Path,
302        input: impl AsRef<Path>,
303    ) -> Result<Self, FsPathParseError> {
304        let input = input.as_ref();
305        let abs_input_path = file_util::normalize_path(&cwd.join(input));
306        let repo_relative_path = file_util::relative_path(base, &abs_input_path);
307        Self::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError {
308            base: file_util::relative_path(cwd, base).into(),
309            input: input.into(),
310            source,
311        })
312    }
313
314    /// Consumes this and returns the underlying string representation.
315    pub fn into_internal_string(self) -> String {
316        self.value
317    }
318}
319
320impl RepoPath {
321    /// Returns repository path pointing to the root.
322    pub const fn root() -> &'static Self {
323        Self::from_internal_string_unchecked("")
324    }
325
326    /// Wraps valid string representation as `RepoPath`.
327    ///
328    /// Returns an error if the input `value` contains empty path component. For
329    /// example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all invalid.
330    pub fn from_internal_string(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
331        if is_valid_repo_path_str(value) {
332            Ok(Self::from_internal_string_unchecked(value))
333        } else {
334            Err(InvalidNewRepoPathError {
335                value: value.to_owned(),
336            })
337        }
338    }
339
340    #[ref_cast_custom]
341    const fn from_internal_string_unchecked(value: &str) -> &Self;
342
343    /// The full string form used internally, not for presenting to users (where
344    /// we may want to use the platform's separator). This format includes a
345    /// trailing slash, unless this path represents the root directory. That
346    /// way it can be concatenated with a basename and produce a valid path.
347    pub fn to_internal_dir_string(&self) -> String {
348        if self.value.is_empty() {
349            String::new()
350        } else {
351            [&self.value, "/"].concat()
352        }
353    }
354
355    /// The full string form used internally, not for presenting to users (where
356    /// we may want to use the platform's separator).
357    pub fn as_internal_file_string(&self) -> &str {
358        &self.value
359    }
360
361    /// Converts repository path to filesystem path relative to the `base`.
362    ///
363    /// The returned path should never contain `..`, `C:` (on Windows), etc.
364    /// However, it may contain reserved working-copy directories such as `.jj`.
365    pub fn to_fs_path(&self, base: &Path) -> Result<PathBuf, InvalidRepoPathError> {
366        let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
367        result.push(base);
368        for c in self.components() {
369            result.push(c.to_fs_name().map_err(|err| err.with_path(self))?);
370        }
371        if result.as_os_str().is_empty() {
372            result.push(".");
373        }
374        Ok(result)
375    }
376
377    /// Converts repository path to filesystem path relative to the `base`,
378    /// without checking invalid path components.
379    ///
380    /// The returned path may point outside of the `base` directory. Use this
381    /// function only for displaying or testing purposes.
382    pub fn to_fs_path_unchecked(&self, base: &Path) -> PathBuf {
383        let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
384        result.push(base);
385        result.extend(self.components().map(RepoPathComponent::as_internal_str));
386        if result.as_os_str().is_empty() {
387            result.push(".");
388        }
389        result
390    }
391
392    pub fn is_root(&self) -> bool {
393        self.value.is_empty()
394    }
395
396    /// Returns true if the `base` is a prefix of this path.
397    pub fn starts_with(&self, base: &Self) -> bool {
398        self.strip_prefix(base).is_some()
399    }
400
401    /// Returns the remaining path with the `base` path removed.
402    pub fn strip_prefix(&self, base: &Self) -> Option<&Self> {
403        if base.value.is_empty() {
404            Some(self)
405        } else {
406            let tail = self.value.strip_prefix(&base.value)?;
407            if tail.is_empty() {
408                Some(Self::from_internal_string_unchecked(tail))
409            } else {
410                tail.strip_prefix('/')
411                    .map(Self::from_internal_string_unchecked)
412            }
413        }
414    }
415
416    /// Returns the parent path without the base name component.
417    pub fn parent(&self) -> Option<&Self> {
418        self.split().map(|(parent, _)| parent)
419    }
420
421    /// Splits this into the parent path and base name component.
422    pub fn split(&self) -> Option<(&Self, &RepoPathComponent)> {
423        let mut components = self.components();
424        let basename = components.next_back()?;
425        Some((components.as_path(), basename))
426    }
427
428    pub fn components(&self) -> RepoPathComponentsIter<'_> {
429        RepoPathComponentsIter { value: &self.value }
430    }
431
432    pub fn ancestors(&self) -> impl Iterator<Item = &Self> {
433        std::iter::successors(Some(self), |path| path.parent())
434    }
435
436    pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf {
437        let value = if self.value.is_empty() {
438            entry.as_internal_str().to_owned()
439        } else {
440            [&self.value, "/", entry.as_internal_str()].concat()
441        };
442        RepoPathBuf { value }
443    }
444
445    /// Splits this path at its common prefix with `other`.
446    ///
447    /// # Returns
448    ///
449    /// Returns the `(common_prefix, self_remainder)`.
450    ///
451    /// All paths will at least have `RepoPath::root()` as a common prefix,
452    /// therefore even if `self` and `other` have no matching parent component
453    /// this function will always return at least `(RepoPath::root(), self)`.
454    ///
455    ///
456    /// # Examples
457    ///
458    /// ```
459    /// use jj_lib::repo_path::RepoPath;
460    ///
461    /// let bing_path = RepoPath::from_internal_string("foo/bar/bing").unwrap();
462    ///
463    /// let baz_path = RepoPath::from_internal_string("foo/bar/baz").unwrap();
464    ///
465    /// let foo_bar_path = RepoPath::from_internal_string("foo/bar").unwrap();
466    ///
467    /// assert_eq!(
468    ///     bing_path.split_common_prefix(&baz_path),
469    ///     (foo_bar_path, RepoPath::from_internal_string("bing").unwrap())
470    /// );
471    ///
472    /// let unrelated_path = RepoPath::from_internal_string("no/common/prefix").unwrap();
473    /// assert_eq!(
474    ///     baz_path.split_common_prefix(&unrelated_path),
475    ///     (RepoPath::root(), baz_path)
476    /// );
477    /// ```
478    pub fn split_common_prefix(&self, other: &Self) -> (&Self, &Self) {
479        // Obtain the common prefix between these paths
480        let mut prefix_len = 0;
481
482        let common_components = self
483            .components()
484            .zip(other.components())
485            .take_while(|(prev_comp, this_comp)| prev_comp == this_comp);
486
487        for (self_comp, _other_comp) in common_components {
488            if prefix_len > 0 {
489                // + 1 for all paths to take their separators into account.
490                // We skip the first one since there are ComponentCount - 1 separators in a
491                // path.
492                prefix_len += 1;
493            }
494
495            prefix_len += self_comp.value.len();
496        }
497
498        if prefix_len == 0 {
499            // No common prefix except root
500            return (Self::root(), self);
501        }
502
503        if prefix_len == self.value.len() {
504            return (self, Self::root());
505        }
506
507        let common_prefix = Self::from_internal_string_unchecked(&self.value[..prefix_len]);
508        let remainder = Self::from_internal_string_unchecked(&self.value[prefix_len + 1..]);
509
510        (common_prefix, remainder)
511    }
512}
513
514impl AsRef<Self> for RepoPath {
515    fn as_ref(&self) -> &Self {
516        self
517    }
518}
519
520impl AsRef<RepoPath> for RepoPathBuf {
521    fn as_ref(&self) -> &RepoPath {
522        self
523    }
524}
525
526impl Borrow<RepoPath> for RepoPathBuf {
527    fn borrow(&self) -> &RepoPath {
528        self
529    }
530}
531
532impl Deref for RepoPathBuf {
533    type Target = RepoPath;
534
535    fn deref(&self) -> &Self::Target {
536        RepoPath::from_internal_string_unchecked(&self.value)
537    }
538}
539
540impl ToOwned for RepoPath {
541    type Owned = RepoPathBuf;
542
543    fn to_owned(&self) -> Self::Owned {
544        let value = self.value.to_owned();
545        RepoPathBuf { value }
546    }
547
548    fn clone_into(&self, target: &mut Self::Owned) {
549        self.value.clone_into(&mut target.value);
550    }
551}
552
553impl Ord for RepoPath {
554    fn cmp(&self, other: &Self) -> Ordering {
555        // If there were leading/trailing slash, components-based Ord would
556        // disagree with str-based Eq.
557        debug_assert!(is_valid_repo_path_str(&self.value));
558        self.components().cmp(other.components())
559    }
560}
561
562impl Ord for RepoPathBuf {
563    fn cmp(&self, other: &Self) -> Ordering {
564        <RepoPath as Ord>::cmp(self, other)
565    }
566}
567
568impl PartialOrd for RepoPath {
569    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
570        Some(self.cmp(other))
571    }
572}
573
574impl PartialOrd for RepoPathBuf {
575    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
576        Some(self.cmp(other))
577    }
578}
579
580impl<P: AsRef<RepoPathComponent>> Extend<P> for RepoPathBuf {
581    fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
582        for component in iter {
583            if !self.value.is_empty() {
584                self.value.push('/');
585            }
586            self.value.push_str(component.as_ref().as_internal_str());
587        }
588    }
589}
590
591/// `RepoPath` contained invalid file/directory component such as `..`.
592#[derive(Clone, Debug, Eq, Error, PartialEq)]
593#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())]
594pub struct InvalidRepoPathError {
595    /// Path containing an error.
596    pub path: RepoPathBuf,
597    /// Source error.
598    pub source: InvalidRepoPathComponentError,
599}
600
601/// `RepoPath` component was invalid. (e.g. `..`)
602#[derive(Clone, Debug, Eq, Error, PartialEq)]
603#[error(r#"Invalid path component "{component}""#)]
604pub struct InvalidRepoPathComponentError {
605    pub component: Box<str>,
606}
607
608impl InvalidRepoPathComponentError {
609    /// Attaches the `path` that caused the error.
610    pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError {
611        InvalidRepoPathError {
612            path: path.to_owned(),
613            source: self,
614        }
615    }
616}
617
618#[derive(Clone, Debug, Eq, Error, PartialEq)]
619pub enum RelativePathParseError {
620    #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
621    InvalidComponent {
622        component: Box<str>,
623        path: Box<Path>,
624    },
625    #[error(r#"Not valid UTF-8 path "{path}""#)]
626    InvalidUtf8 { path: Box<Path> },
627}
628
629#[derive(Clone, Debug, Eq, Error, PartialEq)]
630#[error(r#"Path "{input}" is not in the repo "{base}""#)]
631pub struct FsPathParseError {
632    /// Repository or workspace root path relative to the `cwd`.
633    pub base: Box<Path>,
634    /// Input path without normalization.
635    pub input: Box<Path>,
636    /// Source error.
637    pub source: RelativePathParseError,
638}
639
640fn is_valid_repo_path_component_str(value: &str) -> bool {
641    !value.is_empty() && !value.contains('/')
642}
643
644fn is_valid_repo_path_str(value: &str) -> bool {
645    !value.starts_with('/') && !value.ends_with('/') && !value.contains("//")
646}
647
648/// An error from `RepoPathUiConverter::parse_file_path`.
649#[derive(Debug, Error)]
650pub enum UiPathParseError {
651    #[error(transparent)]
652    Fs(FsPathParseError),
653}
654
655/// Converts `RepoPath`s to and from plain strings as displayed to the user
656/// (e.g. relative to CWD).
657#[derive(Debug, Clone)]
658pub enum RepoPathUiConverter {
659    /// Variant for a local file system. Paths are interpreted relative to `cwd`
660    /// with the repo rooted in `base`.
661    ///
662    /// The `cwd` and `base` paths are supposed to be absolute and normalized in
663    /// the same manner.
664    Fs { cwd: PathBuf, base: PathBuf },
665    // TODO: Add a no-op variant that uses the internal `RepoPath` representation. Can be useful
666    // on a server.
667}
668
669impl RepoPathUiConverter {
670    /// Format a path for display in the UI.
671    pub fn format_file_path(&self, file: &RepoPath) -> String {
672        match self {
673            Self::Fs { cwd, base } => {
674                file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
675                    .display()
676                    .to_string()
677            }
678        }
679    }
680
681    /// Format a copy from `source` to `target` for display in the UI by
682    /// extracting common components and producing something like
683    /// "common/prefix/{source => target}/common/suffix".
684    ///
685    /// If `source == target`, returns `format_file_path(source)`.
686    pub fn format_copied_path(&self, source: &RepoPath, target: &RepoPath) -> String {
687        if source == target {
688            return self.format_file_path(source);
689        }
690        let mut formatted = String::new();
691        match self {
692            Self::Fs { cwd, base } => {
693                let source_path = file_util::relative_path(cwd, &source.to_fs_path_unchecked(base));
694                let target_path = file_util::relative_path(cwd, &target.to_fs_path_unchecked(base));
695
696                let source_components = source_path.components().collect_vec();
697                let target_components = target_path.components().collect_vec();
698
699                let prefix_count = source_components
700                    .iter()
701                    .zip(target_components.iter())
702                    .take_while(|(source_component, target_component)| {
703                        source_component == target_component
704                    })
705                    .count()
706                    .min(source_components.len().saturating_sub(1))
707                    .min(target_components.len().saturating_sub(1));
708
709                let suffix_count = source_components
710                    .iter()
711                    .skip(prefix_count)
712                    .rev()
713                    .zip(target_components.iter().skip(prefix_count).rev())
714                    .take_while(|(source_component, target_component)| {
715                        source_component == target_component
716                    })
717                    .count()
718                    .min(source_components.len().saturating_sub(1))
719                    .min(target_components.len().saturating_sub(1));
720
721                fn format_components(c: &[std::path::Component]) -> String {
722                    c.iter().collect::<PathBuf>().display().to_string()
723                }
724
725                if prefix_count > 0 {
726                    formatted.push_str(&format_components(&source_components[0..prefix_count]));
727                    formatted.push_str(std::path::MAIN_SEPARATOR_STR);
728                }
729                formatted.push('{');
730                formatted.push_str(&format_components(
731                    &source_components
732                        [prefix_count..(source_components.len() - suffix_count).max(prefix_count)],
733                ));
734                formatted.push_str(" => ");
735                formatted.push_str(&format_components(
736                    &target_components
737                        [prefix_count..(target_components.len() - suffix_count).max(prefix_count)],
738                ));
739                formatted.push('}');
740                if suffix_count > 0 {
741                    formatted.push_str(std::path::MAIN_SEPARATOR_STR);
742                    formatted.push_str(&format_components(
743                        &source_components[source_components.len() - suffix_count..],
744                    ));
745                }
746            }
747        }
748        formatted
749    }
750
751    /// Parses a path from the UI.
752    ///
753    /// It's up to the implementation whether absolute paths are allowed, and
754    /// where relative paths are interpreted as relative to.
755    pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
756        match self {
757            Self::Fs { cwd, base } => {
758                RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
759            }
760        }
761    }
762}
763
764#[cfg(test)]
765mod tests {
766    use std::panic;
767
768    use assert_matches::assert_matches;
769    use itertools::Itertools as _;
770
771    use super::*;
772    use crate::tests::new_temp_dir;
773
774    fn repo_path(value: &str) -> &RepoPath {
775        RepoPath::from_internal_string(value).unwrap()
776    }
777
778    fn repo_path_component(value: &str) -> &RepoPathComponent {
779        RepoPathComponent::new(value).unwrap()
780    }
781
782    #[test]
783    fn test_is_root() {
784        assert!(RepoPath::root().is_root());
785        assert!(repo_path("").is_root());
786        assert!(!repo_path("foo").is_root());
787    }
788
789    #[test]
790    fn test_from_internal_string() {
791        let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
792        assert_eq!(repo_path_buf(""), RepoPathBuf::root());
793        assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
794        assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
795        assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
796        assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
797
798        assert_eq!(repo_path(""), RepoPath::root());
799        assert!(panic::catch_unwind(|| repo_path("/")).is_err());
800        assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
801        assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
802        assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
803    }
804
805    #[test]
806    fn test_as_internal_file_string() {
807        assert_eq!(RepoPath::root().as_internal_file_string(), "");
808        assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
809        assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
810    }
811
812    #[test]
813    fn test_to_internal_dir_string() {
814        assert_eq!(RepoPath::root().to_internal_dir_string(), "");
815        assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
816        assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
817    }
818
819    #[test]
820    fn test_starts_with() {
821        assert!(repo_path("").starts_with(repo_path("")));
822        assert!(repo_path("x").starts_with(repo_path("")));
823        assert!(!repo_path("").starts_with(repo_path("x")));
824
825        assert!(repo_path("x").starts_with(repo_path("x")));
826        assert!(repo_path("x/y").starts_with(repo_path("x")));
827        assert!(!repo_path("xy").starts_with(repo_path("x")));
828        assert!(!repo_path("x/y").starts_with(repo_path("y")));
829
830        assert!(repo_path("x/y").starts_with(repo_path("x/y")));
831        assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
832        assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
833        assert!(!repo_path("x").starts_with(repo_path("x/y")));
834        assert!(!repo_path("xy").starts_with(repo_path("x/y")));
835    }
836
837    #[test]
838    fn test_strip_prefix() {
839        assert_eq!(
840            repo_path("").strip_prefix(repo_path("")),
841            Some(repo_path(""))
842        );
843        assert_eq!(
844            repo_path("x").strip_prefix(repo_path("")),
845            Some(repo_path("x"))
846        );
847        assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
848
849        assert_eq!(
850            repo_path("x").strip_prefix(repo_path("x")),
851            Some(repo_path(""))
852        );
853        assert_eq!(
854            repo_path("x/y").strip_prefix(repo_path("x")),
855            Some(repo_path("y"))
856        );
857        assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
858        assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
859
860        assert_eq!(
861            repo_path("x/y").strip_prefix(repo_path("x/y")),
862            Some(repo_path(""))
863        );
864        assert_eq!(
865            repo_path("x/y/z").strip_prefix(repo_path("x/y")),
866            Some(repo_path("z"))
867        );
868        assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
869        assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
870        assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
871    }
872
873    #[test]
874    fn test_order() {
875        assert!(RepoPath::root() < repo_path("dir"));
876        assert!(repo_path("dir") < repo_path("dirx"));
877        // '#' < '/', but ["dir", "sub"] < ["dir#"]
878        assert!(repo_path("dir") < repo_path("dir#"));
879        assert!(repo_path("dir") < repo_path("dir/sub"));
880        assert!(repo_path("dir/sub") < repo_path("dir#"));
881
882        assert!(repo_path("abc") < repo_path("dir/file"));
883        assert!(repo_path("dir") < repo_path("dir/file"));
884        assert!(repo_path("dis") > repo_path("dir/file"));
885        assert!(repo_path("xyz") > repo_path("dir/file"));
886        assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
887    }
888
889    #[test]
890    fn test_join() {
891        let root = RepoPath::root();
892        let dir = root.join(repo_path_component("dir"));
893        assert_eq!(dir.as_ref(), repo_path("dir"));
894        let subdir = dir.join(repo_path_component("subdir"));
895        assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
896        assert_eq!(
897            subdir.join(repo_path_component("file")).as_ref(),
898            repo_path("dir/subdir/file")
899        );
900    }
901
902    #[test]
903    fn test_extend() {
904        let mut path = RepoPathBuf::root();
905        path.extend(std::iter::empty::<RepoPathComponentBuf>());
906        assert_eq!(path.as_ref(), RepoPath::root());
907        path.extend([repo_path_component("dir")]);
908        assert_eq!(path.as_ref(), repo_path("dir"));
909        path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
910        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
911        path.extend(std::iter::empty::<RepoPathComponentBuf>());
912        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
913    }
914
915    #[test]
916    fn test_parent() {
917        let root = RepoPath::root();
918        let dir_component = repo_path_component("dir");
919        let subdir_component = repo_path_component("subdir");
920
921        let dir = root.join(dir_component);
922        let subdir = dir.join(subdir_component);
923
924        assert_eq!(root.parent(), None);
925        assert_eq!(dir.parent(), Some(root));
926        assert_eq!(subdir.parent(), Some(dir.as_ref()));
927    }
928
929    #[test]
930    fn test_split() {
931        let root = RepoPath::root();
932        let dir_component = repo_path_component("dir");
933        let file_component = repo_path_component("file");
934
935        let dir = root.join(dir_component);
936        let file = dir.join(file_component);
937
938        assert_eq!(root.split(), None);
939        assert_eq!(dir.split(), Some((root, dir_component)));
940        assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
941    }
942
943    #[test]
944    fn test_components() {
945        assert!(RepoPath::root().components().next().is_none());
946        assert_eq!(
947            repo_path("dir").components().collect_vec(),
948            vec![repo_path_component("dir")]
949        );
950        assert_eq!(
951            repo_path("dir/subdir").components().collect_vec(),
952            vec![repo_path_component("dir"), repo_path_component("subdir")]
953        );
954
955        // Iterates from back
956        assert!(RepoPath::root().components().next_back().is_none());
957        assert_eq!(
958            repo_path("dir").components().rev().collect_vec(),
959            vec![repo_path_component("dir")]
960        );
961        assert_eq!(
962            repo_path("dir/subdir").components().rev().collect_vec(),
963            vec![repo_path_component("subdir"), repo_path_component("dir")]
964        );
965    }
966
967    #[test]
968    fn test_ancestors() {
969        assert_eq!(
970            RepoPath::root().ancestors().collect_vec(),
971            vec![RepoPath::root()]
972        );
973        assert_eq!(
974            repo_path("dir").ancestors().collect_vec(),
975            vec![repo_path("dir"), RepoPath::root()]
976        );
977        assert_eq!(
978            repo_path("dir/subdir").ancestors().collect_vec(),
979            vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
980        );
981    }
982
983    #[test]
984    fn test_to_fs_path() {
985        assert_eq!(
986            repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
987            Path::new("base/dir")
988        );
989        assert_eq!(
990            repo_path("").to_fs_path(Path::new("")).unwrap(),
991            Path::new(".")
992        );
993        assert_eq!(
994            repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
995            Path::new("base/dir/file")
996        );
997        assert_eq!(
998            repo_path("some/deep/dir/file")
999                .to_fs_path(Path::new("base/dir"))
1000                .unwrap(),
1001            Path::new("base/dir/some/deep/dir/file")
1002        );
1003        assert_eq!(
1004            repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
1005            Path::new("dir/file")
1006        );
1007
1008        // Current/parent dir component
1009        assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
1010        assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
1011        assert!(
1012            repo_path("dir/../file")
1013                .to_fs_path(Path::new("base"))
1014                .is_err()
1015        );
1016        assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
1017        assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
1018        assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
1019        assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
1020
1021        // Empty component (which is invalid as a repo path)
1022        assert!(
1023            RepoPath::from_internal_string_unchecked("/")
1024                .to_fs_path(Path::new("base"))
1025                .is_err()
1026        );
1027        assert_eq!(
1028            // Iterator omits empty component after "/", which is fine so long
1029            // as the returned path doesn't escape.
1030            RepoPath::from_internal_string_unchecked("a/")
1031                .to_fs_path(Path::new("base"))
1032                .unwrap(),
1033            Path::new("base/a")
1034        );
1035        assert!(
1036            RepoPath::from_internal_string_unchecked("/b")
1037                .to_fs_path(Path::new("base"))
1038                .is_err()
1039        );
1040        assert!(
1041            RepoPath::from_internal_string_unchecked("a//b")
1042                .to_fs_path(Path::new("base"))
1043                .is_err()
1044        );
1045
1046        // Component containing slash (simulating Windows path separator)
1047        assert!(
1048            RepoPathComponent::new_unchecked("wind/ows")
1049                .to_fs_name()
1050                .is_err()
1051        );
1052        assert!(
1053            RepoPathComponent::new_unchecked("./file")
1054                .to_fs_name()
1055                .is_err()
1056        );
1057        assert!(
1058            RepoPathComponent::new_unchecked("file/.")
1059                .to_fs_name()
1060                .is_err()
1061        );
1062        assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
1063
1064        // Windows path separator and drive letter
1065        if cfg!(windows) {
1066            assert!(
1067                repo_path(r#"wind\ows"#)
1068                    .to_fs_path(Path::new("base"))
1069                    .is_err()
1070            );
1071            assert!(
1072                repo_path(r#".\file"#)
1073                    .to_fs_path(Path::new("base"))
1074                    .is_err()
1075            );
1076            assert!(
1077                repo_path(r#"file\."#)
1078                    .to_fs_path(Path::new("base"))
1079                    .is_err()
1080            );
1081            assert!(
1082                repo_path(r#"c:/foo"#)
1083                    .to_fs_path(Path::new("base"))
1084                    .is_err()
1085            );
1086        }
1087    }
1088
1089    #[test]
1090    fn test_to_fs_path_unchecked() {
1091        assert_eq!(
1092            repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1093            Path::new("base/dir")
1094        );
1095        assert_eq!(
1096            repo_path("").to_fs_path_unchecked(Path::new("")),
1097            Path::new(".")
1098        );
1099        assert_eq!(
1100            repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1101            Path::new("base/dir/file")
1102        );
1103        assert_eq!(
1104            repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1105            Path::new("base/dir/some/deep/dir/file")
1106        );
1107        assert_eq!(
1108            repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1109            Path::new("dir/file")
1110        );
1111    }
1112
1113    #[test]
1114    fn parse_fs_path_wc_in_cwd() {
1115        let temp_dir = new_temp_dir();
1116        let cwd_path = temp_dir.path().join("repo");
1117        let wc_path = &cwd_path;
1118
1119        assert_eq!(
1120            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1121            Ok(RepoPath::root())
1122        );
1123        assert_eq!(
1124            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1125            Ok(RepoPath::root())
1126        );
1127        assert_eq!(
1128            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1129            Ok(repo_path("file"))
1130        );
1131        // Both slash and the platform's separator are allowed
1132        assert_eq!(
1133            RepoPathBuf::parse_fs_path(
1134                &cwd_path,
1135                wc_path,
1136                format!("dir{}file", std::path::MAIN_SEPARATOR)
1137            )
1138            .as_deref(),
1139            Ok(repo_path("dir/file"))
1140        );
1141        assert_eq!(
1142            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1143            Ok(repo_path("dir/file"))
1144        );
1145        assert_matches!(
1146            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1147            Err(FsPathParseError {
1148                source: RelativePathParseError::InvalidComponent { .. },
1149                ..
1150            })
1151        );
1152        assert_eq!(
1153            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1154            Ok(RepoPath::root())
1155        );
1156        assert_eq!(
1157            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1158            Ok(repo_path("file"))
1159        );
1160        // Input may be absolute path with ".."
1161        assert_eq!(
1162            RepoPathBuf::parse_fs_path(
1163                &cwd_path,
1164                &cwd_path,
1165                cwd_path.join("../repo").to_str().unwrap()
1166            )
1167            .as_deref(),
1168            Ok(RepoPath::root())
1169        );
1170    }
1171
1172    #[test]
1173    fn parse_fs_path_wc_in_cwd_parent() {
1174        let temp_dir = new_temp_dir();
1175        let cwd_path = temp_dir.path().join("dir");
1176        let wc_path = cwd_path.parent().unwrap().to_path_buf();
1177
1178        assert_eq!(
1179            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1180            Ok(repo_path("dir"))
1181        );
1182        assert_eq!(
1183            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1184            Ok(repo_path("dir"))
1185        );
1186        assert_eq!(
1187            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1188            Ok(repo_path("dir/file"))
1189        );
1190        assert_eq!(
1191            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1192            Ok(repo_path("dir/subdir/file"))
1193        );
1194        assert_eq!(
1195            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1196            Ok(RepoPath::root())
1197        );
1198        assert_matches!(
1199            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1200            Err(FsPathParseError {
1201                source: RelativePathParseError::InvalidComponent { .. },
1202                ..
1203            })
1204        );
1205        assert_eq!(
1206            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1207            Ok(repo_path("other-dir/file"))
1208        );
1209    }
1210
1211    #[test]
1212    fn parse_fs_path_wc_in_cwd_child() {
1213        let temp_dir = new_temp_dir();
1214        let cwd_path = temp_dir.path().join("cwd");
1215        let wc_path = cwd_path.join("repo");
1216
1217        assert_matches!(
1218            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1219            Err(FsPathParseError {
1220                source: RelativePathParseError::InvalidComponent { .. },
1221                ..
1222            })
1223        );
1224        assert_matches!(
1225            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1226            Err(FsPathParseError {
1227                source: RelativePathParseError::InvalidComponent { .. },
1228                ..
1229            })
1230        );
1231        assert_eq!(
1232            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1233            Ok(RepoPath::root())
1234        );
1235        assert_eq!(
1236            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1237            Ok(repo_path("file"))
1238        );
1239        assert_eq!(
1240            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1241            Ok(repo_path("dir/file"))
1242        );
1243    }
1244
1245    #[test]
1246    fn test_format_copied_path() {
1247        let ui = RepoPathUiConverter::Fs {
1248            cwd: PathBuf::from("."),
1249            base: PathBuf::from("."),
1250        };
1251
1252        let format = |before, after| {
1253            ui.format_copied_path(repo_path(before), repo_path(after))
1254                .replace('\\', "/")
1255        };
1256
1257        assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1258        assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1259        assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1260        assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1261        assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1262        assert_eq!(
1263            format("one/two", "one/two/three/one/two"),
1264            "one/{ => two/three/one}/two"
1265        );
1266
1267        assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1268        assert_eq!(
1269            format("one/two/three", "one/four/three"),
1270            "one/{two => four}/three"
1271        );
1272        assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1273        assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1274        assert_eq!(format("two", "four"), "{two => four}");
1275        assert_eq!(format("file1", "file2"), "{file1 => file2}");
1276        assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1277        assert_eq!(
1278            format("x/something/something/2to1.txt", "x/something/2to1.txt"),
1279            "x/something/{something => }/2to1.txt"
1280        );
1281        assert_eq!(
1282            format("x/something/1to2.txt", "x/something/something/1to2.txt"),
1283            "x/something/{ => something}/1to2.txt"
1284        );
1285    }
1286
1287    #[test]
1288    fn test_split_common_prefix() {
1289        assert_eq!(
1290            repo_path("foo/bar").split_common_prefix(repo_path("foo/bar/baz")),
1291            (repo_path("foo/bar"), repo_path(""))
1292        );
1293
1294        assert_eq!(
1295            repo_path("foo/bar/baz").split_common_prefix(repo_path("foo/bar")),
1296            (repo_path("foo/bar"), repo_path("baz"))
1297        );
1298
1299        assert_eq!(
1300            repo_path("foo/bar/bing").split_common_prefix(repo_path("foo/bar/baz")),
1301            (repo_path("foo/bar"), repo_path("bing"))
1302        );
1303
1304        assert_eq!(
1305            repo_path("no/common/prefix").split_common_prefix(repo_path("foo/bar/baz")),
1306            (RepoPath::root(), repo_path("no/common/prefix"))
1307        );
1308
1309        assert_eq!(
1310            repo_path("same/path").split_common_prefix(repo_path("same/path")),
1311            (repo_path("same/path"), RepoPath::root())
1312        );
1313
1314        assert_eq!(
1315            RepoPath::root().split_common_prefix(repo_path("foo")),
1316            (RepoPath::root(), RepoPath::root())
1317        );
1318
1319        assert_eq!(
1320            RepoPath::root().split_common_prefix(RepoPath::root()),
1321            (RepoPath::root(), RepoPath::root())
1322        );
1323
1324        assert_eq!(
1325            repo_path("foo/bar").split_common_prefix(RepoPath::root()),
1326            (RepoPath::root(), repo_path("foo/bar"))
1327        );
1328    }
1329}