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#![allow(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
446impl AsRef<Self> for RepoPath {
447    fn as_ref(&self) -> &Self {
448        self
449    }
450}
451
452impl AsRef<RepoPath> for RepoPathBuf {
453    fn as_ref(&self) -> &RepoPath {
454        self
455    }
456}
457
458impl Borrow<RepoPath> for RepoPathBuf {
459    fn borrow(&self) -> &RepoPath {
460        self
461    }
462}
463
464impl Deref for RepoPathBuf {
465    type Target = RepoPath;
466
467    fn deref(&self) -> &Self::Target {
468        RepoPath::from_internal_string_unchecked(&self.value)
469    }
470}
471
472impl ToOwned for RepoPath {
473    type Owned = RepoPathBuf;
474
475    fn to_owned(&self) -> Self::Owned {
476        let value = self.value.to_owned();
477        RepoPathBuf { value }
478    }
479
480    fn clone_into(&self, target: &mut Self::Owned) {
481        self.value.clone_into(&mut target.value);
482    }
483}
484
485impl Ord for RepoPath {
486    fn cmp(&self, other: &Self) -> Ordering {
487        // If there were leading/trailing slash, components-based Ord would
488        // disagree with str-based Eq.
489        debug_assert!(is_valid_repo_path_str(&self.value));
490        self.components().cmp(other.components())
491    }
492}
493
494impl Ord for RepoPathBuf {
495    fn cmp(&self, other: &Self) -> Ordering {
496        <RepoPath as Ord>::cmp(self, other)
497    }
498}
499
500impl PartialOrd for RepoPath {
501    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
502        Some(self.cmp(other))
503    }
504}
505
506impl PartialOrd for RepoPathBuf {
507    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
508        Some(self.cmp(other))
509    }
510}
511
512impl<P: AsRef<RepoPathComponent>> Extend<P> for RepoPathBuf {
513    fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
514        for component in iter {
515            if !self.value.is_empty() {
516                self.value.push('/');
517            }
518            self.value.push_str(component.as_ref().as_internal_str());
519        }
520    }
521}
522
523/// `RepoPath` contained invalid file/directory component such as `..`.
524#[derive(Clone, Debug, Eq, Error, PartialEq)]
525#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())]
526pub struct InvalidRepoPathError {
527    /// Path containing an error.
528    pub path: RepoPathBuf,
529    /// Source error.
530    pub source: InvalidRepoPathComponentError,
531}
532
533/// `RepoPath` component was invalid. (e.g. `..`)
534#[derive(Clone, Debug, Eq, Error, PartialEq)]
535#[error(r#"Invalid path component "{component}""#)]
536pub struct InvalidRepoPathComponentError {
537    pub component: Box<str>,
538}
539
540impl InvalidRepoPathComponentError {
541    /// Attaches the `path` that caused the error.
542    pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError {
543        InvalidRepoPathError {
544            path: path.to_owned(),
545            source: self,
546        }
547    }
548}
549
550#[derive(Clone, Debug, Eq, Error, PartialEq)]
551pub enum RelativePathParseError {
552    #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
553    InvalidComponent {
554        component: Box<str>,
555        path: Box<Path>,
556    },
557    #[error(r#"Not valid UTF-8 path "{path}""#)]
558    InvalidUtf8 { path: Box<Path> },
559}
560
561#[derive(Clone, Debug, Eq, Error, PartialEq)]
562#[error(r#"Path "{input}" is not in the repo "{base}""#)]
563pub struct FsPathParseError {
564    /// Repository or workspace root path relative to the `cwd`.
565    pub base: Box<Path>,
566    /// Input path without normalization.
567    pub input: Box<Path>,
568    /// Source error.
569    pub source: RelativePathParseError,
570}
571
572fn is_valid_repo_path_component_str(value: &str) -> bool {
573    !value.is_empty() && !value.contains('/')
574}
575
576fn is_valid_repo_path_str(value: &str) -> bool {
577    !value.starts_with('/') && !value.ends_with('/') && !value.contains("//")
578}
579
580/// An error from `RepoPathUiConverter::parse_file_path`.
581#[derive(Debug, Error)]
582pub enum UiPathParseError {
583    #[error(transparent)]
584    Fs(FsPathParseError),
585}
586
587/// Converts `RepoPath`s to and from plain strings as displayed to the user
588/// (e.g. relative to CWD).
589#[derive(Debug, Clone)]
590pub enum RepoPathUiConverter {
591    /// Variant for a local file system. Paths are interpreted relative to `cwd`
592    /// with the repo rooted in `base`.
593    ///
594    /// The `cwd` and `base` paths are supposed to be absolute and normalized in
595    /// the same manner.
596    Fs { cwd: PathBuf, base: PathBuf },
597    // TODO: Add a no-op variant that uses the internal `RepoPath` representation. Can be useful
598    // on a server.
599}
600
601impl RepoPathUiConverter {
602    /// Format a path for display in the UI.
603    pub fn format_file_path(&self, file: &RepoPath) -> String {
604        match self {
605            Self::Fs { cwd, base } => {
606                file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
607                    .display()
608                    .to_string()
609            }
610        }
611    }
612
613    /// Format a copy from `source` to `target` for display in the UI by
614    /// extracting common components and producing something like
615    /// "common/prefix/{source => target}/common/suffix".
616    ///
617    /// If `source == target`, returns `format_file_path(source)`.
618    pub fn format_copied_path(&self, source: &RepoPath, target: &RepoPath) -> String {
619        if source == target {
620            return self.format_file_path(source);
621        }
622        let mut formatted = String::new();
623        match self {
624            Self::Fs { cwd, base } => {
625                let source_path = file_util::relative_path(cwd, &source.to_fs_path_unchecked(base));
626                let target_path = file_util::relative_path(cwd, &target.to_fs_path_unchecked(base));
627
628                let source_components = source_path.components().collect_vec();
629                let target_components = target_path.components().collect_vec();
630
631                let prefix_count = source_components
632                    .iter()
633                    .zip(target_components.iter())
634                    .take_while(|(source_component, target_component)| {
635                        source_component == target_component
636                    })
637                    .count()
638                    .min(source_components.len().saturating_sub(1))
639                    .min(target_components.len().saturating_sub(1));
640
641                let suffix_count = source_components
642                    .iter()
643                    .skip(prefix_count)
644                    .rev()
645                    .zip(target_components.iter().skip(prefix_count).rev())
646                    .take_while(|(source_component, target_component)| {
647                        source_component == target_component
648                    })
649                    .count()
650                    .min(source_components.len().saturating_sub(1))
651                    .min(target_components.len().saturating_sub(1));
652
653                fn format_components(c: &[std::path::Component]) -> String {
654                    c.iter().collect::<PathBuf>().display().to_string()
655                }
656
657                if prefix_count > 0 {
658                    formatted.push_str(&format_components(&source_components[0..prefix_count]));
659                    formatted.push_str(std::path::MAIN_SEPARATOR_STR);
660                }
661                formatted.push('{');
662                formatted.push_str(&format_components(
663                    &source_components
664                        [prefix_count..(source_components.len() - suffix_count).max(prefix_count)],
665                ));
666                formatted.push_str(" => ");
667                formatted.push_str(&format_components(
668                    &target_components
669                        [prefix_count..(target_components.len() - suffix_count).max(prefix_count)],
670                ));
671                formatted.push('}');
672                if suffix_count > 0 {
673                    formatted.push_str(std::path::MAIN_SEPARATOR_STR);
674                    formatted.push_str(&format_components(
675                        &source_components[source_components.len() - suffix_count..],
676                    ));
677                }
678            }
679        }
680        formatted
681    }
682
683    /// Parses a path from the UI.
684    ///
685    /// It's up to the implementation whether absolute paths are allowed, and
686    /// where relative paths are interpreted as relative to.
687    pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
688        match self {
689            Self::Fs { cwd, base } => {
690                RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
691            }
692        }
693    }
694}
695
696#[cfg(test)]
697mod tests {
698    use std::panic;
699
700    use assert_matches::assert_matches;
701    use itertools::Itertools as _;
702
703    use super::*;
704    use crate::tests::new_temp_dir;
705
706    fn repo_path(value: &str) -> &RepoPath {
707        RepoPath::from_internal_string(value).unwrap()
708    }
709
710    fn repo_path_component(value: &str) -> &RepoPathComponent {
711        RepoPathComponent::new(value).unwrap()
712    }
713
714    #[test]
715    fn test_is_root() {
716        assert!(RepoPath::root().is_root());
717        assert!(repo_path("").is_root());
718        assert!(!repo_path("foo").is_root());
719    }
720
721    #[test]
722    fn test_from_internal_string() {
723        let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
724        assert_eq!(repo_path_buf(""), RepoPathBuf::root());
725        assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
726        assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
727        assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
728        assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
729
730        assert_eq!(repo_path(""), RepoPath::root());
731        assert!(panic::catch_unwind(|| repo_path("/")).is_err());
732        assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
733        assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
734        assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
735    }
736
737    #[test]
738    fn test_as_internal_file_string() {
739        assert_eq!(RepoPath::root().as_internal_file_string(), "");
740        assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
741        assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
742    }
743
744    #[test]
745    fn test_to_internal_dir_string() {
746        assert_eq!(RepoPath::root().to_internal_dir_string(), "");
747        assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
748        assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
749    }
750
751    #[test]
752    fn test_starts_with() {
753        assert!(repo_path("").starts_with(repo_path("")));
754        assert!(repo_path("x").starts_with(repo_path("")));
755        assert!(!repo_path("").starts_with(repo_path("x")));
756
757        assert!(repo_path("x").starts_with(repo_path("x")));
758        assert!(repo_path("x/y").starts_with(repo_path("x")));
759        assert!(!repo_path("xy").starts_with(repo_path("x")));
760        assert!(!repo_path("x/y").starts_with(repo_path("y")));
761
762        assert!(repo_path("x/y").starts_with(repo_path("x/y")));
763        assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
764        assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
765        assert!(!repo_path("x").starts_with(repo_path("x/y")));
766        assert!(!repo_path("xy").starts_with(repo_path("x/y")));
767    }
768
769    #[test]
770    fn test_strip_prefix() {
771        assert_eq!(
772            repo_path("").strip_prefix(repo_path("")),
773            Some(repo_path(""))
774        );
775        assert_eq!(
776            repo_path("x").strip_prefix(repo_path("")),
777            Some(repo_path("x"))
778        );
779        assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
780
781        assert_eq!(
782            repo_path("x").strip_prefix(repo_path("x")),
783            Some(repo_path(""))
784        );
785        assert_eq!(
786            repo_path("x/y").strip_prefix(repo_path("x")),
787            Some(repo_path("y"))
788        );
789        assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
790        assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
791
792        assert_eq!(
793            repo_path("x/y").strip_prefix(repo_path("x/y")),
794            Some(repo_path(""))
795        );
796        assert_eq!(
797            repo_path("x/y/z").strip_prefix(repo_path("x/y")),
798            Some(repo_path("z"))
799        );
800        assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
801        assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
802        assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
803    }
804
805    #[test]
806    fn test_order() {
807        assert!(RepoPath::root() < repo_path("dir"));
808        assert!(repo_path("dir") < repo_path("dirx"));
809        // '#' < '/', but ["dir", "sub"] < ["dir#"]
810        assert!(repo_path("dir") < repo_path("dir#"));
811        assert!(repo_path("dir") < repo_path("dir/sub"));
812        assert!(repo_path("dir/sub") < repo_path("dir#"));
813
814        assert!(repo_path("abc") < repo_path("dir/file"));
815        assert!(repo_path("dir") < repo_path("dir/file"));
816        assert!(repo_path("dis") > repo_path("dir/file"));
817        assert!(repo_path("xyz") > repo_path("dir/file"));
818        assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
819    }
820
821    #[test]
822    fn test_join() {
823        let root = RepoPath::root();
824        let dir = root.join(repo_path_component("dir"));
825        assert_eq!(dir.as_ref(), repo_path("dir"));
826        let subdir = dir.join(repo_path_component("subdir"));
827        assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
828        assert_eq!(
829            subdir.join(repo_path_component("file")).as_ref(),
830            repo_path("dir/subdir/file")
831        );
832    }
833
834    #[test]
835    fn test_extend() {
836        let mut path = RepoPathBuf::root();
837        path.extend(std::iter::empty::<RepoPathComponentBuf>());
838        assert_eq!(path.as_ref(), RepoPath::root());
839        path.extend([repo_path_component("dir")]);
840        assert_eq!(path.as_ref(), repo_path("dir"));
841        path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
842        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
843        path.extend(std::iter::empty::<RepoPathComponentBuf>());
844        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
845    }
846
847    #[test]
848    fn test_parent() {
849        let root = RepoPath::root();
850        let dir_component = repo_path_component("dir");
851        let subdir_component = repo_path_component("subdir");
852
853        let dir = root.join(dir_component);
854        let subdir = dir.join(subdir_component);
855
856        assert_eq!(root.parent(), None);
857        assert_eq!(dir.parent(), Some(root));
858        assert_eq!(subdir.parent(), Some(dir.as_ref()));
859    }
860
861    #[test]
862    fn test_split() {
863        let root = RepoPath::root();
864        let dir_component = repo_path_component("dir");
865        let file_component = repo_path_component("file");
866
867        let dir = root.join(dir_component);
868        let file = dir.join(file_component);
869
870        assert_eq!(root.split(), None);
871        assert_eq!(dir.split(), Some((root, dir_component)));
872        assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
873    }
874
875    #[test]
876    fn test_components() {
877        assert!(RepoPath::root().components().next().is_none());
878        assert_eq!(
879            repo_path("dir").components().collect_vec(),
880            vec![repo_path_component("dir")]
881        );
882        assert_eq!(
883            repo_path("dir/subdir").components().collect_vec(),
884            vec![repo_path_component("dir"), repo_path_component("subdir")]
885        );
886
887        // Iterates from back
888        assert!(RepoPath::root().components().next_back().is_none());
889        assert_eq!(
890            repo_path("dir").components().rev().collect_vec(),
891            vec![repo_path_component("dir")]
892        );
893        assert_eq!(
894            repo_path("dir/subdir").components().rev().collect_vec(),
895            vec![repo_path_component("subdir"), repo_path_component("dir")]
896        );
897    }
898
899    #[test]
900    fn test_ancestors() {
901        assert_eq!(
902            RepoPath::root().ancestors().collect_vec(),
903            vec![RepoPath::root()]
904        );
905        assert_eq!(
906            repo_path("dir").ancestors().collect_vec(),
907            vec![repo_path("dir"), RepoPath::root()]
908        );
909        assert_eq!(
910            repo_path("dir/subdir").ancestors().collect_vec(),
911            vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
912        );
913    }
914
915    #[test]
916    fn test_to_fs_path() {
917        assert_eq!(
918            repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
919            Path::new("base/dir")
920        );
921        assert_eq!(
922            repo_path("").to_fs_path(Path::new("")).unwrap(),
923            Path::new(".")
924        );
925        assert_eq!(
926            repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
927            Path::new("base/dir/file")
928        );
929        assert_eq!(
930            repo_path("some/deep/dir/file")
931                .to_fs_path(Path::new("base/dir"))
932                .unwrap(),
933            Path::new("base/dir/some/deep/dir/file")
934        );
935        assert_eq!(
936            repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
937            Path::new("dir/file")
938        );
939
940        // Current/parent dir component
941        assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
942        assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
943        assert!(
944            repo_path("dir/../file")
945                .to_fs_path(Path::new("base"))
946                .is_err()
947        );
948        assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
949        assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
950        assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
951        assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
952
953        // Empty component (which is invalid as a repo path)
954        assert!(
955            RepoPath::from_internal_string_unchecked("/")
956                .to_fs_path(Path::new("base"))
957                .is_err()
958        );
959        assert_eq!(
960            // Iterator omits empty component after "/", which is fine so long
961            // as the returned path doesn't escape.
962            RepoPath::from_internal_string_unchecked("a/")
963                .to_fs_path(Path::new("base"))
964                .unwrap(),
965            Path::new("base/a")
966        );
967        assert!(
968            RepoPath::from_internal_string_unchecked("/b")
969                .to_fs_path(Path::new("base"))
970                .is_err()
971        );
972        assert!(
973            RepoPath::from_internal_string_unchecked("a//b")
974                .to_fs_path(Path::new("base"))
975                .is_err()
976        );
977
978        // Component containing slash (simulating Windows path separator)
979        assert!(
980            RepoPathComponent::new_unchecked("wind/ows")
981                .to_fs_name()
982                .is_err()
983        );
984        assert!(
985            RepoPathComponent::new_unchecked("./file")
986                .to_fs_name()
987                .is_err()
988        );
989        assert!(
990            RepoPathComponent::new_unchecked("file/.")
991                .to_fs_name()
992                .is_err()
993        );
994        assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
995
996        // Windows path separator and drive letter
997        if cfg!(windows) {
998            assert!(
999                repo_path(r#"wind\ows"#)
1000                    .to_fs_path(Path::new("base"))
1001                    .is_err()
1002            );
1003            assert!(
1004                repo_path(r#".\file"#)
1005                    .to_fs_path(Path::new("base"))
1006                    .is_err()
1007            );
1008            assert!(
1009                repo_path(r#"file\."#)
1010                    .to_fs_path(Path::new("base"))
1011                    .is_err()
1012            );
1013            assert!(
1014                repo_path(r#"c:/foo"#)
1015                    .to_fs_path(Path::new("base"))
1016                    .is_err()
1017            );
1018        }
1019    }
1020
1021    #[test]
1022    fn test_to_fs_path_unchecked() {
1023        assert_eq!(
1024            repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1025            Path::new("base/dir")
1026        );
1027        assert_eq!(
1028            repo_path("").to_fs_path_unchecked(Path::new("")),
1029            Path::new(".")
1030        );
1031        assert_eq!(
1032            repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1033            Path::new("base/dir/file")
1034        );
1035        assert_eq!(
1036            repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1037            Path::new("base/dir/some/deep/dir/file")
1038        );
1039        assert_eq!(
1040            repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1041            Path::new("dir/file")
1042        );
1043    }
1044
1045    #[test]
1046    fn parse_fs_path_wc_in_cwd() {
1047        let temp_dir = new_temp_dir();
1048        let cwd_path = temp_dir.path().join("repo");
1049        let wc_path = &cwd_path;
1050
1051        assert_eq!(
1052            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1053            Ok(RepoPath::root())
1054        );
1055        assert_eq!(
1056            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1057            Ok(RepoPath::root())
1058        );
1059        assert_eq!(
1060            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1061            Ok(repo_path("file"))
1062        );
1063        // Both slash and the platform's separator are allowed
1064        assert_eq!(
1065            RepoPathBuf::parse_fs_path(
1066                &cwd_path,
1067                wc_path,
1068                format!("dir{}file", std::path::MAIN_SEPARATOR)
1069            )
1070            .as_deref(),
1071            Ok(repo_path("dir/file"))
1072        );
1073        assert_eq!(
1074            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1075            Ok(repo_path("dir/file"))
1076        );
1077        assert_matches!(
1078            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1079            Err(FsPathParseError {
1080                source: RelativePathParseError::InvalidComponent { .. },
1081                ..
1082            })
1083        );
1084        assert_eq!(
1085            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1086            Ok(RepoPath::root())
1087        );
1088        assert_eq!(
1089            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1090            Ok(repo_path("file"))
1091        );
1092        // Input may be absolute path with ".."
1093        assert_eq!(
1094            RepoPathBuf::parse_fs_path(
1095                &cwd_path,
1096                &cwd_path,
1097                cwd_path.join("../repo").to_str().unwrap()
1098            )
1099            .as_deref(),
1100            Ok(RepoPath::root())
1101        );
1102    }
1103
1104    #[test]
1105    fn parse_fs_path_wc_in_cwd_parent() {
1106        let temp_dir = new_temp_dir();
1107        let cwd_path = temp_dir.path().join("dir");
1108        let wc_path = cwd_path.parent().unwrap().to_path_buf();
1109
1110        assert_eq!(
1111            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1112            Ok(repo_path("dir"))
1113        );
1114        assert_eq!(
1115            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1116            Ok(repo_path("dir"))
1117        );
1118        assert_eq!(
1119            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1120            Ok(repo_path("dir/file"))
1121        );
1122        assert_eq!(
1123            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1124            Ok(repo_path("dir/subdir/file"))
1125        );
1126        assert_eq!(
1127            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1128            Ok(RepoPath::root())
1129        );
1130        assert_matches!(
1131            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1132            Err(FsPathParseError {
1133                source: RelativePathParseError::InvalidComponent { .. },
1134                ..
1135            })
1136        );
1137        assert_eq!(
1138            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1139            Ok(repo_path("other-dir/file"))
1140        );
1141    }
1142
1143    #[test]
1144    fn parse_fs_path_wc_in_cwd_child() {
1145        let temp_dir = new_temp_dir();
1146        let cwd_path = temp_dir.path().join("cwd");
1147        let wc_path = cwd_path.join("repo");
1148
1149        assert_matches!(
1150            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1151            Err(FsPathParseError {
1152                source: RelativePathParseError::InvalidComponent { .. },
1153                ..
1154            })
1155        );
1156        assert_matches!(
1157            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1158            Err(FsPathParseError {
1159                source: RelativePathParseError::InvalidComponent { .. },
1160                ..
1161            })
1162        );
1163        assert_eq!(
1164            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1165            Ok(RepoPath::root())
1166        );
1167        assert_eq!(
1168            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1169            Ok(repo_path("file"))
1170        );
1171        assert_eq!(
1172            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1173            Ok(repo_path("dir/file"))
1174        );
1175    }
1176
1177    #[test]
1178    fn test_format_copied_path() {
1179        let ui = RepoPathUiConverter::Fs {
1180            cwd: PathBuf::from("."),
1181            base: PathBuf::from("."),
1182        };
1183
1184        let format = |before, after| {
1185            ui.format_copied_path(repo_path(before), repo_path(after))
1186                .replace('\\', "/")
1187        };
1188
1189        assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1190        assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1191        assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1192        assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1193        assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1194        assert_eq!(
1195            format("one/two", "one/two/three/one/two"),
1196            "one/{ => two/three/one}/two"
1197        );
1198
1199        assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1200        assert_eq!(
1201            format("one/two/three", "one/four/three"),
1202            "one/{two => four}/three"
1203        );
1204        assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1205        assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1206        assert_eq!(format("two", "four"), "{two => four}");
1207        assert_eq!(format("file1", "file2"), "{file1 => file2}");
1208        assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1209        assert_eq!(
1210            format("x/something/something/2to1.txt", "x/something/2to1.txt"),
1211            "x/something/{something => }/2to1.txt"
1212        );
1213        assert_eq!(
1214            format("x/something/1to2.txt", "x/something/something/1to2.txt"),
1215            "x/something/{ => something}/1to2.txt"
1216        );
1217    }
1218}