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::ref_cast_custom;
30use ref_cast::RefCastCustom;
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<RepoPathComponent> for RepoPathComponent {
118    fn as_ref(&self) -> &RepoPathComponent {
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)]
203pub struct RepoPathBuf {
204    // Don't add more fields. Eq, Hash, and Ord must be compatible with the
205    // borrowed RepoPath type.
206    value: String,
207}
208
209/// Borrowed repository path.
210#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom)]
211#[repr(transparent)]
212pub struct RepoPath {
213    value: str,
214}
215
216impl Debug for RepoPath {
217    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
218        write!(f, "{:?}", &self.value)
219    }
220}
221
222impl Debug for RepoPathBuf {
223    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
224        <RepoPath as Debug>::fmt(self, f)
225    }
226}
227
228/// The `value` is not a valid repo path because it contains empty path
229/// component. For example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all
230/// invalid.
231#[derive(Clone, Debug, Eq, Error, PartialEq)]
232#[error(r#"Invalid repo path input "{value}""#)]
233pub struct InvalidNewRepoPathError {
234    value: String,
235}
236
237impl RepoPathBuf {
238    /// Creates owned repository path pointing to the root.
239    pub const fn root() -> Self {
240        RepoPathBuf {
241            value: String::new(),
242        }
243    }
244
245    /// Creates `RepoPathBuf` from valid string representation.
246    pub fn from_internal_string(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
247        let value: String = value.into();
248        if is_valid_repo_path_str(&value) {
249            Ok(RepoPathBuf { value })
250        } else {
251            Err(InvalidNewRepoPathError { value })
252        }
253    }
254
255    /// Converts repo-relative `Path` to `RepoPathBuf`.
256    ///
257    /// The input path should not contain redundant `.` or `..`.
258    pub fn from_relative_path(
259        relative_path: impl AsRef<Path>,
260    ) -> Result<Self, RelativePathParseError> {
261        let relative_path = relative_path.as_ref();
262        if relative_path == Path::new(".") {
263            return Ok(Self::root());
264        }
265
266        let mut components = relative_path
267            .components()
268            .map(|c| match c {
269                Component::Normal(name) => {
270                    name.to_str()
271                        .ok_or_else(|| RelativePathParseError::InvalidUtf8 {
272                            path: relative_path.into(),
273                        })
274                }
275                _ => Err(RelativePathParseError::InvalidComponent {
276                    component: c.as_os_str().to_string_lossy().into(),
277                    path: relative_path.into(),
278                }),
279            })
280            .fuse();
281        let mut value = String::with_capacity(relative_path.as_os_str().len());
282        if let Some(name) = components.next() {
283            value.push_str(name?);
284        }
285        for name in components {
286            value.push('/');
287            value.push_str(name?);
288        }
289        Ok(RepoPathBuf { value })
290    }
291
292    /// Parses an `input` path into a `RepoPathBuf` relative to `base`.
293    ///
294    /// The `cwd` and `base` paths are supposed to be absolute and normalized in
295    /// the same manner. The `input` path may be either relative to `cwd` or
296    /// absolute.
297    pub fn parse_fs_path(
298        cwd: &Path,
299        base: &Path,
300        input: impl AsRef<Path>,
301    ) -> Result<Self, FsPathParseError> {
302        let input = input.as_ref();
303        let abs_input_path = file_util::normalize_path(&cwd.join(input));
304        let repo_relative_path = file_util::relative_path(base, &abs_input_path);
305        Self::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError {
306            base: file_util::relative_path(cwd, base).into(),
307            input: input.into(),
308            source,
309        })
310    }
311
312    /// Consumes this and returns the underlying string representation.
313    pub fn into_internal_string(self) -> String {
314        self.value
315    }
316}
317
318impl RepoPath {
319    /// Returns repository path pointing to the root.
320    pub const fn root() -> &'static Self {
321        Self::from_internal_string_unchecked("")
322    }
323
324    /// Wraps valid string representation as `RepoPath`.
325    ///
326    /// Returns an error if the input `value` contains empty path component. For
327    /// example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all invalid.
328    pub fn from_internal_string(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
329        if is_valid_repo_path_str(value) {
330            Ok(Self::from_internal_string_unchecked(value))
331        } else {
332            Err(InvalidNewRepoPathError {
333                value: value.to_owned(),
334            })
335        }
336    }
337
338    #[ref_cast_custom]
339    const fn from_internal_string_unchecked(value: &str) -> &Self;
340
341    /// The full string form used internally, not for presenting to users (where
342    /// we may want to use the platform's separator). This format includes a
343    /// trailing slash, unless this path represents the root directory. That
344    /// way it can be concatenated with a basename and produce a valid path.
345    pub fn to_internal_dir_string(&self) -> String {
346        if self.value.is_empty() {
347            String::new()
348        } else {
349            [&self.value, "/"].concat()
350        }
351    }
352
353    /// The full string form used internally, not for presenting to users (where
354    /// we may want to use the platform's separator).
355    pub fn as_internal_file_string(&self) -> &str {
356        &self.value
357    }
358
359    /// Converts repository path to filesystem path relative to the `base`.
360    ///
361    /// The returned path should never contain `..`, `C:` (on Windows), etc.
362    /// However, it may contain reserved working-copy directories such as `.jj`.
363    pub fn to_fs_path(&self, base: &Path) -> Result<PathBuf, InvalidRepoPathError> {
364        let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
365        result.push(base);
366        for c in self.components() {
367            result.push(c.to_fs_name().map_err(|err| err.with_path(self))?);
368        }
369        if result.as_os_str().is_empty() {
370            result.push(".");
371        }
372        Ok(result)
373    }
374
375    /// Converts repository path to filesystem path relative to the `base`,
376    /// without checking invalid path components.
377    ///
378    /// The returned path may point outside of the `base` directory. Use this
379    /// function only for displaying or testing purposes.
380    pub fn to_fs_path_unchecked(&self, base: &Path) -> PathBuf {
381        let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
382        result.push(base);
383        result.extend(self.components().map(RepoPathComponent::as_internal_str));
384        if result.as_os_str().is_empty() {
385            result.push(".");
386        }
387        result
388    }
389
390    pub fn is_root(&self) -> bool {
391        self.value.is_empty()
392    }
393
394    /// Returns true if the `base` is a prefix of this path.
395    pub fn starts_with(&self, base: &RepoPath) -> bool {
396        self.strip_prefix(base).is_some()
397    }
398
399    /// Returns the remaining path with the `base` path removed.
400    pub fn strip_prefix(&self, base: &RepoPath) -> Option<&RepoPath> {
401        if base.value.is_empty() {
402            Some(self)
403        } else {
404            let tail = self.value.strip_prefix(&base.value)?;
405            if tail.is_empty() {
406                Some(RepoPath::from_internal_string_unchecked(tail))
407            } else {
408                tail.strip_prefix('/')
409                    .map(RepoPath::from_internal_string_unchecked)
410            }
411        }
412    }
413
414    /// Returns the parent path without the base name component.
415    pub fn parent(&self) -> Option<&RepoPath> {
416        self.split().map(|(parent, _)| parent)
417    }
418
419    /// Splits this into the parent path and base name component.
420    pub fn split(&self) -> Option<(&RepoPath, &RepoPathComponent)> {
421        let mut components = self.components();
422        let basename = components.next_back()?;
423        Some((components.as_path(), basename))
424    }
425
426    pub fn components(&self) -> RepoPathComponentsIter<'_> {
427        RepoPathComponentsIter { value: &self.value }
428    }
429
430    pub fn ancestors(&self) -> impl Iterator<Item = &RepoPath> {
431        std::iter::successors(Some(self), |path| path.parent())
432    }
433
434    pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf {
435        let value = if self.value.is_empty() {
436            entry.as_internal_str().to_owned()
437        } else {
438            [&self.value, "/", entry.as_internal_str()].concat()
439        };
440        RepoPathBuf { value }
441    }
442}
443
444impl AsRef<RepoPath> for RepoPath {
445    fn as_ref(&self) -> &RepoPath {
446        self
447    }
448}
449
450impl AsRef<RepoPath> for RepoPathBuf {
451    fn as_ref(&self) -> &RepoPath {
452        self
453    }
454}
455
456impl Borrow<RepoPath> for RepoPathBuf {
457    fn borrow(&self) -> &RepoPath {
458        self
459    }
460}
461
462impl Deref for RepoPathBuf {
463    type Target = RepoPath;
464
465    fn deref(&self) -> &Self::Target {
466        RepoPath::from_internal_string_unchecked(&self.value)
467    }
468}
469
470impl ToOwned for RepoPath {
471    type Owned = RepoPathBuf;
472
473    fn to_owned(&self) -> Self::Owned {
474        let value = self.value.to_owned();
475        RepoPathBuf { value }
476    }
477
478    fn clone_into(&self, target: &mut Self::Owned) {
479        self.value.clone_into(&mut target.value);
480    }
481}
482
483impl Ord for RepoPath {
484    fn cmp(&self, other: &Self) -> Ordering {
485        // If there were leading/trailing slash, components-based Ord would
486        // disagree with str-based Eq.
487        debug_assert!(is_valid_repo_path_str(&self.value));
488        self.components().cmp(other.components())
489    }
490}
491
492impl Ord for RepoPathBuf {
493    fn cmp(&self, other: &Self) -> Ordering {
494        <RepoPath as Ord>::cmp(self, other)
495    }
496}
497
498impl PartialOrd for RepoPath {
499    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
500        Some(self.cmp(other))
501    }
502}
503
504impl PartialOrd for RepoPathBuf {
505    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
506        Some(self.cmp(other))
507    }
508}
509
510impl<P: AsRef<RepoPathComponent>> Extend<P> for RepoPathBuf {
511    fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
512        for component in iter {
513            if !self.value.is_empty() {
514                self.value.push('/');
515            }
516            self.value.push_str(component.as_ref().as_internal_str());
517        }
518    }
519}
520
521/// `RepoPath` contained invalid file/directory component such as `..`.
522#[derive(Clone, Debug, Eq, Error, PartialEq)]
523#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())]
524pub struct InvalidRepoPathError {
525    /// Path containing an error.
526    pub path: RepoPathBuf,
527    /// Source error.
528    pub source: InvalidRepoPathComponentError,
529}
530
531/// `RepoPath` component was invalid. (e.g. `..`)
532#[derive(Clone, Debug, Eq, Error, PartialEq)]
533#[error(r#"Invalid path component "{component}""#)]
534pub struct InvalidRepoPathComponentError {
535    pub component: Box<str>,
536}
537
538impl InvalidRepoPathComponentError {
539    /// Attaches the `path` that caused the error.
540    pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError {
541        InvalidRepoPathError {
542            path: path.to_owned(),
543            source: self,
544        }
545    }
546}
547
548#[derive(Clone, Debug, Eq, Error, PartialEq)]
549pub enum RelativePathParseError {
550    #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
551    InvalidComponent {
552        component: Box<str>,
553        path: Box<Path>,
554    },
555    #[error(r#"Not valid UTF-8 path "{path}""#)]
556    InvalidUtf8 { path: Box<Path> },
557}
558
559#[derive(Clone, Debug, Eq, Error, PartialEq)]
560#[error(r#"Path "{input}" is not in the repo "{base}""#)]
561pub struct FsPathParseError {
562    /// Repository or workspace root path relative to the `cwd`.
563    pub base: Box<Path>,
564    /// Input path without normalization.
565    pub input: Box<Path>,
566    /// Source error.
567    pub source: RelativePathParseError,
568}
569
570fn is_valid_repo_path_component_str(value: &str) -> bool {
571    !value.is_empty() && !value.contains('/')
572}
573
574fn is_valid_repo_path_str(value: &str) -> bool {
575    !value.starts_with('/') && !value.ends_with('/') && !value.contains("//")
576}
577
578/// An error from `RepoPathUiConverter::parse_file_path`.
579#[derive(Debug, Error)]
580pub enum UiPathParseError {
581    #[error(transparent)]
582    Fs(FsPathParseError),
583}
584
585/// Converts `RepoPath`s to and from plain strings as displayed to the user
586/// (e.g. relative to CWD).
587#[derive(Debug, Clone)]
588pub enum RepoPathUiConverter {
589    /// Variant for a local file system. Paths are interpreted relative to `cwd`
590    /// with the repo rooted in `base`.
591    ///
592    /// The `cwd` and `base` paths are supposed to be absolute and normalized in
593    /// the same manner.
594    Fs { cwd: PathBuf, base: PathBuf },
595    // TODO: Add a no-op variant that uses the internal `RepoPath` representation. Can be useful
596    // on a server.
597}
598
599impl RepoPathUiConverter {
600    /// Format a path for display in the UI.
601    pub fn format_file_path(&self, file: &RepoPath) -> String {
602        match self {
603            RepoPathUiConverter::Fs { cwd, base } => {
604                file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
605                    .to_str()
606                    .unwrap()
607                    .to_owned()
608            }
609        }
610    }
611
612    /// Format a copy from `source` to `target` for display in the UI by
613    /// extracting common components and producing something like
614    /// "common/prefix/{source => target}/common/suffix".
615    ///
616    /// If `source == target`, returns `format_file_path(source)`.
617    pub fn format_copied_path(&self, source: &RepoPath, target: &RepoPath) -> String {
618        if source == target {
619            return self.format_file_path(source);
620        }
621        let mut formatted = String::new();
622        match self {
623            RepoPathUiConverter::Fs { cwd, base } => {
624                let source_path = file_util::relative_path(cwd, &source.to_fs_path_unchecked(base));
625                let target_path = file_util::relative_path(cwd, &target.to_fs_path_unchecked(base));
626
627                let source_components = source_path.components().collect_vec();
628                let target_components = target_path.components().collect_vec();
629
630                let prefix_count = source_components
631                    .iter()
632                    .zip(target_components.iter())
633                    .take_while(|(source_component, target_component)| {
634                        source_component == target_component
635                    })
636                    .count()
637                    .min(source_components.len().saturating_sub(1))
638                    .min(target_components.len().saturating_sub(1));
639
640                let suffix_count = source_components
641                    .iter()
642                    .rev()
643                    .zip(target_components.iter().rev())
644                    .take_while(|(source_component, target_component)| {
645                        source_component == target_component
646                    })
647                    .count()
648                    .min(source_components.len().saturating_sub(1))
649                    .min(target_components.len().saturating_sub(1));
650
651                fn format_components(c: &[std::path::Component]) -> String {
652                    c.iter().collect::<PathBuf>().to_str().unwrap().to_owned()
653                }
654
655                if prefix_count > 0 {
656                    formatted.push_str(&format_components(&source_components[0..prefix_count]));
657                    formatted.push_str(std::path::MAIN_SEPARATOR_STR);
658                }
659                formatted.push('{');
660                formatted.push_str(&format_components(
661                    &source_components
662                        [prefix_count..(source_components.len() - suffix_count).max(prefix_count)],
663                ));
664                formatted.push_str(" => ");
665                formatted.push_str(&format_components(
666                    &target_components
667                        [prefix_count..(target_components.len() - suffix_count).max(prefix_count)],
668                ));
669                formatted.push('}');
670                if suffix_count > 0 {
671                    formatted.push_str(std::path::MAIN_SEPARATOR_STR);
672                    formatted.push_str(&format_components(
673                        &source_components[source_components.len() - suffix_count..],
674                    ));
675                }
676            }
677        }
678        formatted
679    }
680
681    /// Parses a path from the UI.
682    ///
683    /// It's up to the implementation whether absolute paths are allowed, and
684    /// where relative paths are interpreted as relative to.
685    pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
686        match self {
687            RepoPathUiConverter::Fs { cwd, base } => {
688                RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
689            }
690        }
691    }
692}
693
694#[cfg(test)]
695mod tests {
696    use std::panic;
697
698    use assert_matches::assert_matches;
699    use itertools::Itertools as _;
700
701    use super::*;
702    use crate::tests::new_temp_dir;
703
704    fn repo_path(value: &str) -> &RepoPath {
705        RepoPath::from_internal_string(value).unwrap()
706    }
707
708    fn repo_path_component(value: &str) -> &RepoPathComponent {
709        RepoPathComponent::new(value).unwrap()
710    }
711
712    #[test]
713    fn test_is_root() {
714        assert!(RepoPath::root().is_root());
715        assert!(repo_path("").is_root());
716        assert!(!repo_path("foo").is_root());
717    }
718
719    #[test]
720    fn test_from_internal_string() {
721        let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
722        assert_eq!(repo_path_buf(""), RepoPathBuf::root());
723        assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
724        assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
725        assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
726        assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
727
728        assert_eq!(repo_path(""), RepoPath::root());
729        assert!(panic::catch_unwind(|| repo_path("/")).is_err());
730        assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
731        assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
732        assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
733    }
734
735    #[test]
736    fn test_as_internal_file_string() {
737        assert_eq!(RepoPath::root().as_internal_file_string(), "");
738        assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
739        assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
740    }
741
742    #[test]
743    fn test_to_internal_dir_string() {
744        assert_eq!(RepoPath::root().to_internal_dir_string(), "");
745        assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
746        assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
747    }
748
749    #[test]
750    fn test_starts_with() {
751        assert!(repo_path("").starts_with(repo_path("")));
752        assert!(repo_path("x").starts_with(repo_path("")));
753        assert!(!repo_path("").starts_with(repo_path("x")));
754
755        assert!(repo_path("x").starts_with(repo_path("x")));
756        assert!(repo_path("x/y").starts_with(repo_path("x")));
757        assert!(!repo_path("xy").starts_with(repo_path("x")));
758        assert!(!repo_path("x/y").starts_with(repo_path("y")));
759
760        assert!(repo_path("x/y").starts_with(repo_path("x/y")));
761        assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
762        assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
763        assert!(!repo_path("x").starts_with(repo_path("x/y")));
764        assert!(!repo_path("xy").starts_with(repo_path("x/y")));
765    }
766
767    #[test]
768    fn test_strip_prefix() {
769        assert_eq!(
770            repo_path("").strip_prefix(repo_path("")),
771            Some(repo_path(""))
772        );
773        assert_eq!(
774            repo_path("x").strip_prefix(repo_path("")),
775            Some(repo_path("x"))
776        );
777        assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
778
779        assert_eq!(
780            repo_path("x").strip_prefix(repo_path("x")),
781            Some(repo_path(""))
782        );
783        assert_eq!(
784            repo_path("x/y").strip_prefix(repo_path("x")),
785            Some(repo_path("y"))
786        );
787        assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
788        assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
789
790        assert_eq!(
791            repo_path("x/y").strip_prefix(repo_path("x/y")),
792            Some(repo_path(""))
793        );
794        assert_eq!(
795            repo_path("x/y/z").strip_prefix(repo_path("x/y")),
796            Some(repo_path("z"))
797        );
798        assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
799        assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
800        assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
801    }
802
803    #[test]
804    fn test_order() {
805        assert!(RepoPath::root() < repo_path("dir"));
806        assert!(repo_path("dir") < repo_path("dirx"));
807        // '#' < '/', but ["dir", "sub"] < ["dir#"]
808        assert!(repo_path("dir") < repo_path("dir#"));
809        assert!(repo_path("dir") < repo_path("dir/sub"));
810        assert!(repo_path("dir/sub") < repo_path("dir#"));
811
812        assert!(repo_path("abc") < repo_path("dir/file"));
813        assert!(repo_path("dir") < repo_path("dir/file"));
814        assert!(repo_path("dis") > repo_path("dir/file"));
815        assert!(repo_path("xyz") > repo_path("dir/file"));
816        assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
817    }
818
819    #[test]
820    fn test_join() {
821        let root = RepoPath::root();
822        let dir = root.join(repo_path_component("dir"));
823        assert_eq!(dir.as_ref(), repo_path("dir"));
824        let subdir = dir.join(repo_path_component("subdir"));
825        assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
826        assert_eq!(
827            subdir.join(repo_path_component("file")).as_ref(),
828            repo_path("dir/subdir/file")
829        );
830    }
831
832    #[test]
833    fn test_extend() {
834        let mut path = RepoPathBuf::root();
835        path.extend(std::iter::empty::<RepoPathComponentBuf>());
836        assert_eq!(path.as_ref(), RepoPath::root());
837        path.extend([repo_path_component("dir")]);
838        assert_eq!(path.as_ref(), repo_path("dir"));
839        path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
840        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
841        path.extend(std::iter::empty::<RepoPathComponentBuf>());
842        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
843    }
844
845    #[test]
846    fn test_parent() {
847        let root = RepoPath::root();
848        let dir_component = repo_path_component("dir");
849        let subdir_component = repo_path_component("subdir");
850
851        let dir = root.join(dir_component);
852        let subdir = dir.join(subdir_component);
853
854        assert_eq!(root.parent(), None);
855        assert_eq!(dir.parent(), Some(root));
856        assert_eq!(subdir.parent(), Some(dir.as_ref()));
857    }
858
859    #[test]
860    fn test_split() {
861        let root = RepoPath::root();
862        let dir_component = repo_path_component("dir");
863        let file_component = repo_path_component("file");
864
865        let dir = root.join(dir_component);
866        let file = dir.join(file_component);
867
868        assert_eq!(root.split(), None);
869        assert_eq!(dir.split(), Some((root, dir_component)));
870        assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
871    }
872
873    #[test]
874    fn test_components() {
875        assert!(RepoPath::root().components().next().is_none());
876        assert_eq!(
877            repo_path("dir").components().collect_vec(),
878            vec![repo_path_component("dir")]
879        );
880        assert_eq!(
881            repo_path("dir/subdir").components().collect_vec(),
882            vec![repo_path_component("dir"), repo_path_component("subdir")]
883        );
884
885        // Iterates from back
886        assert!(RepoPath::root().components().next_back().is_none());
887        assert_eq!(
888            repo_path("dir").components().rev().collect_vec(),
889            vec![repo_path_component("dir")]
890        );
891        assert_eq!(
892            repo_path("dir/subdir").components().rev().collect_vec(),
893            vec![repo_path_component("subdir"), repo_path_component("dir")]
894        );
895    }
896
897    #[test]
898    fn test_ancestors() {
899        assert_eq!(
900            RepoPath::root().ancestors().collect_vec(),
901            vec![RepoPath::root()]
902        );
903        assert_eq!(
904            repo_path("dir").ancestors().collect_vec(),
905            vec![repo_path("dir"), RepoPath::root()]
906        );
907        assert_eq!(
908            repo_path("dir/subdir").ancestors().collect_vec(),
909            vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
910        );
911    }
912
913    #[test]
914    fn test_to_fs_path() {
915        assert_eq!(
916            repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
917            Path::new("base/dir")
918        );
919        assert_eq!(
920            repo_path("").to_fs_path(Path::new("")).unwrap(),
921            Path::new(".")
922        );
923        assert_eq!(
924            repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
925            Path::new("base/dir/file")
926        );
927        assert_eq!(
928            repo_path("some/deep/dir/file")
929                .to_fs_path(Path::new("base/dir"))
930                .unwrap(),
931            Path::new("base/dir/some/deep/dir/file")
932        );
933        assert_eq!(
934            repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
935            Path::new("dir/file")
936        );
937
938        // Current/parent dir component
939        assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
940        assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
941        assert!(repo_path("dir/../file")
942            .to_fs_path(Path::new("base"))
943            .is_err());
944        assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
945        assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
946        assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
947        assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
948
949        // Empty component (which is invalid as a repo path)
950        assert!(RepoPath::from_internal_string_unchecked("/")
951            .to_fs_path(Path::new("base"))
952            .is_err());
953        assert_eq!(
954            // Iterator omits empty component after "/", which is fine so long
955            // as the returned path doesn't escape.
956            RepoPath::from_internal_string_unchecked("a/")
957                .to_fs_path(Path::new("base"))
958                .unwrap(),
959            Path::new("base/a")
960        );
961        assert!(RepoPath::from_internal_string_unchecked("/b")
962            .to_fs_path(Path::new("base"))
963            .is_err());
964        assert!(RepoPath::from_internal_string_unchecked("a//b")
965            .to_fs_path(Path::new("base"))
966            .is_err());
967
968        // Component containing slash (simulating Windows path separator)
969        assert!(RepoPathComponent::new_unchecked("wind/ows")
970            .to_fs_name()
971            .is_err());
972        assert!(RepoPathComponent::new_unchecked("./file")
973            .to_fs_name()
974            .is_err());
975        assert!(RepoPathComponent::new_unchecked("file/.")
976            .to_fs_name()
977            .is_err());
978        assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
979
980        // Windows path separator and drive letter
981        if cfg!(windows) {
982            assert!(repo_path(r#"wind\ows"#)
983                .to_fs_path(Path::new("base"))
984                .is_err());
985            assert!(repo_path(r#".\file"#)
986                .to_fs_path(Path::new("base"))
987                .is_err());
988            assert!(repo_path(r#"file\."#)
989                .to_fs_path(Path::new("base"))
990                .is_err());
991            assert!(repo_path(r#"c:/foo"#)
992                .to_fs_path(Path::new("base"))
993                .is_err());
994        }
995    }
996
997    #[test]
998    fn test_to_fs_path_unchecked() {
999        assert_eq!(
1000            repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1001            Path::new("base/dir")
1002        );
1003        assert_eq!(
1004            repo_path("").to_fs_path_unchecked(Path::new("")),
1005            Path::new(".")
1006        );
1007        assert_eq!(
1008            repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1009            Path::new("base/dir/file")
1010        );
1011        assert_eq!(
1012            repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1013            Path::new("base/dir/some/deep/dir/file")
1014        );
1015        assert_eq!(
1016            repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1017            Path::new("dir/file")
1018        );
1019    }
1020
1021    #[test]
1022    fn parse_fs_path_wc_in_cwd() {
1023        let temp_dir = new_temp_dir();
1024        let cwd_path = temp_dir.path().join("repo");
1025        let wc_path = &cwd_path;
1026
1027        assert_eq!(
1028            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1029            Ok(RepoPath::root())
1030        );
1031        assert_eq!(
1032            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1033            Ok(RepoPath::root())
1034        );
1035        assert_eq!(
1036            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1037            Ok(repo_path("file"))
1038        );
1039        // Both slash and the platform's separator are allowed
1040        assert_eq!(
1041            RepoPathBuf::parse_fs_path(
1042                &cwd_path,
1043                wc_path,
1044                format!("dir{}file", std::path::MAIN_SEPARATOR)
1045            )
1046            .as_deref(),
1047            Ok(repo_path("dir/file"))
1048        );
1049        assert_eq!(
1050            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1051            Ok(repo_path("dir/file"))
1052        );
1053        assert_matches!(
1054            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1055            Err(FsPathParseError {
1056                source: RelativePathParseError::InvalidComponent { .. },
1057                ..
1058            })
1059        );
1060        assert_eq!(
1061            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1062            Ok(RepoPath::root())
1063        );
1064        assert_eq!(
1065            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1066            Ok(repo_path("file"))
1067        );
1068        // Input may be absolute path with ".."
1069        assert_eq!(
1070            RepoPathBuf::parse_fs_path(
1071                &cwd_path,
1072                &cwd_path,
1073                cwd_path.join("../repo").to_str().unwrap()
1074            )
1075            .as_deref(),
1076            Ok(RepoPath::root())
1077        );
1078    }
1079
1080    #[test]
1081    fn parse_fs_path_wc_in_cwd_parent() {
1082        let temp_dir = new_temp_dir();
1083        let cwd_path = temp_dir.path().join("dir");
1084        let wc_path = cwd_path.parent().unwrap().to_path_buf();
1085
1086        assert_eq!(
1087            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1088            Ok(repo_path("dir"))
1089        );
1090        assert_eq!(
1091            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1092            Ok(repo_path("dir"))
1093        );
1094        assert_eq!(
1095            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1096            Ok(repo_path("dir/file"))
1097        );
1098        assert_eq!(
1099            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1100            Ok(repo_path("dir/subdir/file"))
1101        );
1102        assert_eq!(
1103            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1104            Ok(RepoPath::root())
1105        );
1106        assert_matches!(
1107            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1108            Err(FsPathParseError {
1109                source: RelativePathParseError::InvalidComponent { .. },
1110                ..
1111            })
1112        );
1113        assert_eq!(
1114            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1115            Ok(repo_path("other-dir/file"))
1116        );
1117    }
1118
1119    #[test]
1120    fn parse_fs_path_wc_in_cwd_child() {
1121        let temp_dir = new_temp_dir();
1122        let cwd_path = temp_dir.path().join("cwd");
1123        let wc_path = cwd_path.join("repo");
1124
1125        assert_matches!(
1126            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1127            Err(FsPathParseError {
1128                source: RelativePathParseError::InvalidComponent { .. },
1129                ..
1130            })
1131        );
1132        assert_matches!(
1133            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1134            Err(FsPathParseError {
1135                source: RelativePathParseError::InvalidComponent { .. },
1136                ..
1137            })
1138        );
1139        assert_eq!(
1140            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1141            Ok(RepoPath::root())
1142        );
1143        assert_eq!(
1144            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1145            Ok(repo_path("file"))
1146        );
1147        assert_eq!(
1148            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1149            Ok(repo_path("dir/file"))
1150        );
1151    }
1152
1153    #[test]
1154    fn test_format_copied_path() {
1155        let ui = RepoPathUiConverter::Fs {
1156            cwd: PathBuf::from("."),
1157            base: PathBuf::from("."),
1158        };
1159
1160        let format = |before, after| {
1161            ui.format_copied_path(repo_path(before), repo_path(after))
1162                .replace('\\', "/")
1163        };
1164
1165        assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1166        assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1167        assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1168        assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1169        assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1170        assert_eq!(
1171            format("one/two", "one/two/three/one/two"),
1172            "one/{ => two/three/one}/two"
1173        );
1174
1175        assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1176        assert_eq!(
1177            format("one/two/three", "one/four/three"),
1178            "one/{two => four}/three"
1179        );
1180        assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1181        assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1182        assert_eq!(format("two", "four"), "{two => four}");
1183        assert_eq!(format("file1", "file2"), "{file1 => file2}");
1184        assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1185    }
1186}