Skip to main content

jj_core/
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//! A [`RepoPath`] is a path relative to the repo root. It uses forward slashes
16//! as directory separators regardless of platform. It is always valid UTF-8.
17
18use std::borrow::Borrow;
19use std::cmp::Ordering;
20use std::collections::HashMap;
21use std::fmt;
22use std::fmt::Debug;
23use std::fmt::Formatter;
24use std::iter;
25use std::iter::FusedIterator;
26use std::ops::Deref;
27use std::path::Component;
28use std::path::Path;
29use std::path::PathBuf;
30
31use itertools::Itertools as _;
32use ref_cast::RefCastCustom;
33use ref_cast::ref_cast_custom;
34use thiserror::Error;
35
36use crate::content_hash::ContentHash;
37
38/// Owned `RepoPath` component.
39#[derive(ContentHash, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
40pub struct RepoPathComponentBuf {
41    // Don't add more fields. Eq, Hash, and Ord must be compatible with the
42    // borrowed RepoPathComponent type.
43    value: String,
44}
45
46impl RepoPathComponentBuf {
47    /// Wraps `value` as `RepoPathComponentBuf`.
48    ///
49    /// Returns an error if the input `value` is empty or contains path
50    /// separator.
51    pub fn new(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
52        let value: String = value.into();
53        if is_valid_repo_path_component_str(&value) {
54            Ok(Self { value })
55        } else {
56            Err(InvalidNewRepoPathError { value })
57        }
58    }
59}
60
61/// Borrowed `RepoPath` component.
62#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, RefCastCustom)]
63#[repr(transparent)]
64pub struct RepoPathComponent {
65    value: str,
66}
67
68impl RepoPathComponent {
69    /// Wraps `value` as `RepoPathComponent`.
70    ///
71    /// Returns an error if the input `value` is empty or contains path
72    /// separator.
73    pub fn new(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
74        if is_valid_repo_path_component_str(value) {
75            Ok(Self::new_unchecked(value))
76        } else {
77            Err(InvalidNewRepoPathError {
78                value: value.to_string(),
79            })
80        }
81    }
82
83    #[ref_cast_custom]
84    const fn new_unchecked(value: &str) -> &Self;
85
86    /// Returns the underlying string representation.
87    pub fn as_internal_str(&self) -> &str {
88        &self.value
89    }
90
91    /// Returns a normal filesystem entry name if this path component is valid
92    /// as a file/directory name.
93    pub fn to_fs_name(&self) -> Result<&str, InvalidRepoPathComponentError> {
94        let mut components = Path::new(&self.value).components().fuse();
95        match (components.next(), components.next()) {
96            // Trailing "." can be normalized by Path::components(), so compare
97            // component name. e.g. "foo\." (on Windows) should be rejected.
98            (Some(Component::Normal(name)), None) if name == &self.value => Ok(&self.value),
99            // e.g. ".", "..", "foo\bar" (on Windows)
100            _ => Err(InvalidRepoPathComponentError {
101                component: self.value.into(),
102            }),
103        }
104    }
105}
106
107impl Debug for RepoPathComponent {
108    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
109        write!(f, "{:?}", &self.value)
110    }
111}
112
113impl Debug for RepoPathComponentBuf {
114    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
115        <RepoPathComponent as Debug>::fmt(self, f)
116    }
117}
118
119impl AsRef<Self> for RepoPathComponent {
120    fn as_ref(&self) -> &Self {
121        self
122    }
123}
124
125impl AsRef<RepoPathComponent> for RepoPathComponentBuf {
126    fn as_ref(&self) -> &RepoPathComponent {
127        self
128    }
129}
130
131impl Borrow<RepoPathComponent> for RepoPathComponentBuf {
132    fn borrow(&self) -> &RepoPathComponent {
133        self
134    }
135}
136
137impl Deref for RepoPathComponentBuf {
138    type Target = RepoPathComponent;
139
140    fn deref(&self) -> &Self::Target {
141        RepoPathComponent::new_unchecked(&self.value)
142    }
143}
144
145impl ToOwned for RepoPathComponent {
146    type Owned = RepoPathComponentBuf;
147
148    fn to_owned(&self) -> Self::Owned {
149        let value = self.value.to_owned();
150        RepoPathComponentBuf { value }
151    }
152
153    fn clone_into(&self, target: &mut Self::Owned) {
154        self.value.clone_into(&mut target.value);
155    }
156}
157
158/// Iterator over `RepoPath` components.
159#[derive(Clone, Debug)]
160pub struct RepoPathComponentsIter<'a> {
161    value: &'a str,
162}
163
164impl<'a> RepoPathComponentsIter<'a> {
165    /// Returns the remaining part as repository path.
166    pub fn as_path(&self) -> &'a RepoPath {
167        RepoPath::from_internal_string_unchecked(self.value)
168    }
169}
170
171impl<'a> Iterator for RepoPathComponentsIter<'a> {
172    type Item = &'a RepoPathComponent;
173
174    fn next(&mut self) -> Option<Self::Item> {
175        if self.value.is_empty() {
176            return None;
177        }
178        let (name, remainder) = self
179            .value
180            .split_once('/')
181            .unwrap_or_else(|| (self.value, &self.value[self.value.len()..]));
182        self.value = remainder;
183        Some(RepoPathComponent::new_unchecked(name))
184    }
185}
186
187impl DoubleEndedIterator for RepoPathComponentsIter<'_> {
188    fn next_back(&mut self) -> Option<Self::Item> {
189        if self.value.is_empty() {
190            return None;
191        }
192        let (remainder, name) = self
193            .value
194            .rsplit_once('/')
195            .unwrap_or_else(|| (&self.value[..0], self.value));
196        self.value = remainder;
197        Some(RepoPathComponent::new_unchecked(name))
198    }
199}
200
201impl FusedIterator for RepoPathComponentsIter<'_> {}
202
203/// Owned repository path.
204#[derive(ContentHash, Clone, Eq, Hash, PartialEq, serde::Serialize)]
205#[serde(transparent)]
206pub struct RepoPathBuf {
207    // Don't add more fields. Eq, Hash, and Ord must be compatible with the
208    // borrowed RepoPath type.
209    value: String,
210}
211
212/// Borrowed repository path.
213#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom, serde::Serialize)]
214#[repr(transparent)]
215#[serde(transparent)]
216pub struct RepoPath {
217    value: str,
218}
219
220impl Debug for RepoPath {
221    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
222        write!(f, "{:?}", &self.value)
223    }
224}
225
226impl Debug for RepoPathBuf {
227    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
228        <RepoPath as Debug>::fmt(self, f)
229    }
230}
231
232/// The `value` is not a valid repo path because it contains empty path
233/// component. For example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all
234/// invalid.
235#[derive(Clone, Debug, Eq, Error, PartialEq)]
236#[error(r#"Invalid repo path input "{value}""#)]
237pub struct InvalidNewRepoPathError {
238    value: String,
239}
240
241impl RepoPathBuf {
242    /// Creates owned repository path pointing to the root.
243    pub const fn root() -> Self {
244        Self {
245            value: String::new(),
246        }
247    }
248
249    /// Creates `RepoPathBuf` from valid string representation.
250    pub fn from_internal_string(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
251        let value: String = value.into();
252        if is_valid_repo_path_str(&value) {
253            Ok(Self { value })
254        } else {
255            Err(InvalidNewRepoPathError { value })
256        }
257    }
258
259    /// Converts repo-relative `Path` to `RepoPathBuf`.
260    ///
261    /// The input path should not contain redundant `.` or `..`.
262    pub fn from_relative_path(
263        relative_path: impl AsRef<Path>,
264    ) -> Result<Self, RelativePathParseError> {
265        let relative_path = relative_path.as_ref();
266        if relative_path == Path::new(".") {
267            return Ok(Self::root());
268        }
269
270        let mut components = relative_path
271            .components()
272            .map(|c| match c {
273                Component::Normal(name) => {
274                    name.to_str()
275                        .ok_or_else(|| RelativePathParseError::InvalidUtf8 {
276                            path: relative_path.into(),
277                        })
278                }
279                _ => Err(RelativePathParseError::InvalidComponent {
280                    component: c.as_os_str().to_string_lossy().into(),
281                    path: relative_path.into(),
282                }),
283            })
284            .fuse();
285        let mut value = String::with_capacity(relative_path.as_os_str().len());
286        if let Some(name) = components.next() {
287            value.push_str(name?);
288        }
289        for name in components {
290            value.push('/');
291            value.push_str(name?);
292        }
293        Ok(Self { value })
294    }
295
296    /// Consumes this and returns the underlying string representation.
297    pub fn into_internal_string(self) -> String {
298        self.value
299    }
300}
301
302impl RepoPath {
303    /// Returns repository path pointing to the root.
304    pub const fn root() -> &'static Self {
305        Self::from_internal_string_unchecked("")
306    }
307
308    /// Wraps valid string representation as `RepoPath`.
309    ///
310    /// Returns an error if the input `value` contains empty path component. For
311    /// example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all invalid.
312    pub fn from_internal_string(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
313        if is_valid_repo_path_str(value) {
314            Ok(Self::from_internal_string_unchecked(value))
315        } else {
316            Err(InvalidNewRepoPathError {
317                value: value.to_owned(),
318            })
319        }
320    }
321
322    #[ref_cast_custom]
323    const fn from_internal_string_unchecked(value: &str) -> &Self;
324
325    /// The full string form used internally, not for presenting to users (where
326    /// we may want to use the platform's separator). This format includes a
327    /// trailing slash, unless this path represents the root directory. That
328    /// way it can be concatenated with a basename and produce a valid path.
329    pub fn to_internal_dir_string(&self) -> String {
330        if self.value.is_empty() {
331            String::new()
332        } else {
333            [&self.value, "/"].concat()
334        }
335    }
336
337    /// The full string form used internally, not for presenting to users (where
338    /// we may want to use the platform's separator).
339    pub fn as_internal_file_string(&self) -> &str {
340        &self.value
341    }
342
343    /// Converts repository path to filesystem path relative to the `base`.
344    ///
345    /// The returned path should never contain `..`, `C:` (on Windows), etc.
346    /// However, it may contain reserved working-copy directories such as `.jj`.
347    pub fn to_fs_path(&self, base: &Path) -> Result<PathBuf, InvalidRepoPathError> {
348        let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
349        result.push(base);
350        for c in self.components() {
351            result.push(c.to_fs_name().map_err(|err| err.with_path(self))?);
352        }
353        if result.as_os_str().is_empty() {
354            result.push(".");
355        }
356        Ok(result)
357    }
358
359    /// Converts repository path to filesystem path relative to the `base`,
360    /// without checking invalid path components.
361    ///
362    /// The returned path may point outside of the `base` directory. Use this
363    /// function only for displaying or testing purposes.
364    pub fn to_fs_path_unchecked(&self, base: &Path) -> PathBuf {
365        let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
366        result.push(base);
367        result.extend(self.components().map(RepoPathComponent::as_internal_str));
368        if result.as_os_str().is_empty() {
369            result.push(".");
370        }
371        result
372    }
373
374    /// Returns true if this is a root path.
375    pub fn is_root(&self) -> bool {
376        self.value.is_empty()
377    }
378
379    /// Returns true if the `base` is a prefix of this path.
380    pub fn starts_with(&self, base: &Self) -> bool {
381        self.strip_prefix(base).is_some()
382    }
383
384    /// Returns the remaining path with the `base` path removed.
385    pub fn strip_prefix(&self, base: &Self) -> Option<&Self> {
386        if base.value.is_empty() {
387            Some(self)
388        } else {
389            let tail = self.value.strip_prefix(&base.value)?;
390            if tail.is_empty() {
391                Some(Self::from_internal_string_unchecked(tail))
392            } else {
393                tail.strip_prefix('/')
394                    .map(Self::from_internal_string_unchecked)
395            }
396        }
397    }
398
399    /// Returns the parent path without the base name component.
400    pub fn parent(&self) -> Option<&Self> {
401        self.split().map(|(parent, _)| parent)
402    }
403
404    /// Splits this into the parent path and base name component.
405    pub fn split(&self) -> Option<(&Self, &RepoPathComponent)> {
406        let mut components = self.components();
407        let basename = components.next_back()?;
408        Some((components.as_path(), basename))
409    }
410
411    /// Iterator over the path's components, with parents before children.
412    ///
413    /// For example, `RepoPath::from_internal_string("a/b/c")?.components()`
414    /// yields "a", "b", "c".
415    pub fn components(&self) -> RepoPathComponentsIter<'_> {
416        RepoPathComponentsIter { value: &self.value }
417    }
418
419    /// Iterator over the path's ancestors, with children before parents.
420    ///
421    /// For example, `RepoPath::from_internal_string("a/b/c")?.ancestors()`
422    /// yiels "a/b/c", "a/b", "a", "".
423    pub fn ancestors(&self) -> impl Iterator<Item = &Self> {
424        std::iter::successors(Some(self), |path| path.parent())
425    }
426
427    /// Join the given `entry` on the Path returning a new `RepoPathBuf`.
428    pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf {
429        let value = if self.value.is_empty() {
430            entry.as_internal_str().to_owned()
431        } else {
432            [&self.value, "/", entry.as_internal_str()].concat()
433        };
434        RepoPathBuf { value }
435    }
436
437    /// Splits this path at its common prefix with `other`.
438    ///
439    /// # Returns
440    ///
441    /// Returns the `(common_prefix, self_remainder)`.
442    ///
443    /// All paths will at least have `RepoPath::root()` as a common prefix,
444    /// therefore even if `self` and `other` have no matching parent component
445    /// this function will always return at least `(RepoPath::root(), self)`.
446    ///
447    ///
448    /// # Examples
449    ///
450    /// ```
451    /// use jj_core::repo_path::RepoPath;
452    ///
453    /// let bing_path = RepoPath::from_internal_string("foo/bar/bing").unwrap();
454    ///
455    /// let baz_path = RepoPath::from_internal_string("foo/bar/baz").unwrap();
456    ///
457    /// let foo_bar_path = RepoPath::from_internal_string("foo/bar").unwrap();
458    ///
459    /// assert_eq!(
460    ///     bing_path.split_common_prefix(&baz_path),
461    ///     (foo_bar_path, RepoPath::from_internal_string("bing").unwrap())
462    /// );
463    ///
464    /// let unrelated_path = RepoPath::from_internal_string("no/common/prefix").unwrap();
465    /// assert_eq!(
466    ///     baz_path.split_common_prefix(&unrelated_path),
467    ///     (RepoPath::root(), baz_path)
468    /// );
469    /// ```
470    pub fn split_common_prefix(&self, other: &Self) -> (&Self, &Self) {
471        // Obtain the common prefix between these paths
472        let mut prefix_len = 0;
473
474        let common_components = self
475            .components()
476            .zip(other.components())
477            .take_while(|(prev_comp, this_comp)| prev_comp == this_comp);
478
479        for (self_comp, _other_comp) in common_components {
480            if prefix_len > 0 {
481                // + 1 for all paths to take their separators into account.
482                // We skip the first one since there are ComponentCount - 1 separators in a
483                // path.
484                prefix_len += 1;
485            }
486
487            prefix_len += self_comp.value.len();
488        }
489
490        if prefix_len == 0 {
491            // No common prefix except root
492            return (Self::root(), self);
493        }
494
495        if prefix_len == self.value.len() {
496            return (self, Self::root());
497        }
498
499        let common_prefix = Self::from_internal_string_unchecked(&self.value[..prefix_len]);
500        let remainder = Self::from_internal_string_unchecked(&self.value[prefix_len + 1..]);
501
502        (common_prefix, remainder)
503    }
504}
505
506impl AsRef<Self> for RepoPath {
507    fn as_ref(&self) -> &Self {
508        self
509    }
510}
511
512impl AsRef<RepoPath> for RepoPathBuf {
513    fn as_ref(&self) -> &RepoPath {
514        self
515    }
516}
517
518impl Borrow<RepoPath> for RepoPathBuf {
519    fn borrow(&self) -> &RepoPath {
520        self
521    }
522}
523
524impl Deref for RepoPathBuf {
525    type Target = RepoPath;
526
527    fn deref(&self) -> &Self::Target {
528        RepoPath::from_internal_string_unchecked(&self.value)
529    }
530}
531
532impl ToOwned for RepoPath {
533    type Owned = RepoPathBuf;
534
535    fn to_owned(&self) -> Self::Owned {
536        let value = self.value.to_owned();
537        RepoPathBuf { value }
538    }
539
540    fn clone_into(&self, target: &mut Self::Owned) {
541        self.value.clone_into(&mut target.value);
542    }
543}
544
545impl Ord for RepoPath {
546    fn cmp(&self, other: &Self) -> Ordering {
547        // If there were leading/trailing slash, components-based Ord would
548        // disagree with str-based Eq.
549        debug_assert!(is_valid_repo_path_str(&self.value));
550        self.components().cmp(other.components())
551    }
552}
553
554impl Ord for RepoPathBuf {
555    fn cmp(&self, other: &Self) -> Ordering {
556        <RepoPath as Ord>::cmp(self, other)
557    }
558}
559
560impl PartialOrd for RepoPath {
561    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
562        Some(self.cmp(other))
563    }
564}
565
566impl PartialOrd for RepoPathBuf {
567    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
568        Some(self.cmp(other))
569    }
570}
571
572impl<P: AsRef<RepoPathComponent>> Extend<P> for RepoPathBuf {
573    fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
574        for component in iter {
575            if !self.value.is_empty() {
576                self.value.push('/');
577            }
578            self.value.push_str(component.as_ref().as_internal_str());
579        }
580    }
581}
582
583/// `RepoPath` contained invalid file/directory component such as `..`.
584#[derive(Clone, Debug, Eq, Error, PartialEq)]
585#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())]
586pub struct InvalidRepoPathError {
587    /// Path containing an error.
588    pub path: RepoPathBuf,
589    /// Source error.
590    pub source: InvalidRepoPathComponentError,
591}
592
593/// `RepoPath` component was invalid. (e.g. `..`)
594#[derive(Clone, Debug, Eq, Error, PartialEq)]
595#[error(r#"Invalid path component "{component}""#)]
596pub struct InvalidRepoPathComponentError {
597    /// The invalid component.
598    pub component: Box<str>,
599}
600
601impl InvalidRepoPathComponentError {
602    /// Attaches the `path` that caused the error.
603    pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError {
604        InvalidRepoPathError {
605            path: path.to_owned(),
606            source: self,
607        }
608    }
609}
610
611/// An error which occurs during relative path parsing.
612#[derive(Clone, Debug, Eq, Error, PartialEq)]
613pub enum RelativePathParseError {
614    /// An invalid component was seen.
615    #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
616    InvalidComponent {
617        /// The invalid component.
618        component: Box<str>,
619        /// The path it was a component of.
620        path: Box<Path>,
621    },
622    /// The path was not UTF-8.
623    #[error(r#"Not valid UTF-8 path "{path}""#)]
624    InvalidUtf8 {
625        /// The path which did not contain UTF-8 characters.
626        path: Box<Path>,
627    },
628}
629
630fn is_valid_repo_path_component_str(value: &str) -> bool {
631    !value.is_empty() && !value.contains('/')
632}
633
634fn is_valid_repo_path_str(value: &str) -> bool {
635    !value.starts_with('/') && !value.ends_with('/') && !value.contains("//")
636}
637
638/// Tree that maps `RepoPath` to value of type `V`.
639#[derive(Clone, Default, Eq, PartialEq)]
640pub struct RepoPathTree<V> {
641    entries: HashMap<RepoPathComponentBuf, Self>,
642    value: V,
643}
644
645impl<V> RepoPathTree<V> {
646    /// The value associated with this path.
647    pub fn value(&self) -> &V {
648        &self.value
649    }
650
651    /// Mutable reference to the value associated with this path.
652    pub fn value_mut(&mut self) -> &mut V {
653        &mut self.value
654    }
655
656    /// Set the value associated with this path.
657    pub fn set_value(&mut self, value: V) {
658        self.value = value;
659    }
660
661    /// The immediate children of this node.
662    pub fn children(&self) -> impl Iterator<Item = (&RepoPathComponent, &Self)> {
663        self.entries
664            .iter()
665            .map(|(component, value)| (component.as_ref(), value))
666    }
667
668    /// Whether this node has any children.
669    pub fn has_children(&self) -> bool {
670        !self.entries.is_empty()
671    }
672
673    /// Add a path to the tree. Normally called on the root tree.
674    pub fn add(&mut self, path: &RepoPath) -> &mut Self
675    where
676        V: Default,
677    {
678        path.components().fold(self, |sub, name| {
679            // Avoid name.clone() if entry already exists.
680            if !sub.entries.contains_key(name) {
681                sub.entries.insert(name.to_owned(), Self::default());
682            }
683            sub.entries.get_mut(name).unwrap()
684        })
685    }
686
687    /// Get a reference to the node for the given `path`, if it exists in the
688    /// tree.
689    pub fn get(&self, path: &RepoPath) -> Option<&Self> {
690        path.components()
691            .try_fold(self, |sub, name| sub.entries.get(name))
692    }
693
694    /// Walks the tree from the root to the given `path`, yielding each sub tree
695    /// and remaining path.
696    pub fn walk_to<'a, 'b>(
697        &'a self,
698        path: &'b RepoPath,
699    ) -> impl Iterator<Item = (&'a Self, &'b RepoPath)> {
700        iter::successors(Some((self, path)), |(sub, path)| {
701            let mut components = path.components();
702            let name = components.next()?;
703            Some((sub.entries.get(name)?, components.as_path()))
704        })
705    }
706}
707
708impl<V: Debug> Debug for RepoPathTree<V> {
709    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710        self.value.fmt(f)?;
711        f.write_str(" ")?;
712        f.debug_map()
713            .entries(
714                self.entries
715                    .iter()
716                    .sorted_unstable_by_key(|&(name, _)| name),
717            )
718            .finish()
719    }
720}
721
722#[cfg(test)]
723mod tests {
724    use std::panic;
725
726    use super::*;
727
728    fn repo_path(value: &str) -> &RepoPath {
729        RepoPath::from_internal_string(value).unwrap()
730    }
731
732    fn repo_path_component(value: &str) -> &RepoPathComponent {
733        RepoPathComponent::new(value).unwrap()
734    }
735
736    #[test]
737    fn test_is_root() {
738        assert!(RepoPath::root().is_root());
739        assert!(repo_path("").is_root());
740        assert!(!repo_path("foo").is_root());
741    }
742
743    #[test]
744    fn test_from_internal_string() {
745        let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
746        assert_eq!(repo_path_buf(""), RepoPathBuf::root());
747        assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
748        assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
749        assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
750        assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
751
752        assert_eq!(repo_path(""), RepoPath::root());
753        assert!(panic::catch_unwind(|| repo_path("/")).is_err());
754        assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
755        assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
756        assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
757    }
758
759    #[test]
760    fn test_as_internal_file_string() {
761        assert_eq!(RepoPath::root().as_internal_file_string(), "");
762        assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
763        assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
764    }
765
766    #[test]
767    fn test_to_internal_dir_string() {
768        assert_eq!(RepoPath::root().to_internal_dir_string(), "");
769        assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
770        assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
771    }
772
773    #[test]
774    fn test_starts_with() {
775        assert!(repo_path("").starts_with(repo_path("")));
776        assert!(repo_path("x").starts_with(repo_path("")));
777        assert!(!repo_path("").starts_with(repo_path("x")));
778
779        assert!(repo_path("x").starts_with(repo_path("x")));
780        assert!(repo_path("x/y").starts_with(repo_path("x")));
781        assert!(!repo_path("xy").starts_with(repo_path("x")));
782        assert!(!repo_path("x/y").starts_with(repo_path("y")));
783
784        assert!(repo_path("x/y").starts_with(repo_path("x/y")));
785        assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
786        assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
787        assert!(!repo_path("x").starts_with(repo_path("x/y")));
788        assert!(!repo_path("xy").starts_with(repo_path("x/y")));
789    }
790
791    #[test]
792    fn test_strip_prefix() {
793        assert_eq!(
794            repo_path("").strip_prefix(repo_path("")),
795            Some(repo_path(""))
796        );
797        assert_eq!(
798            repo_path("x").strip_prefix(repo_path("")),
799            Some(repo_path("x"))
800        );
801        assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
802
803        assert_eq!(
804            repo_path("x").strip_prefix(repo_path("x")),
805            Some(repo_path(""))
806        );
807        assert_eq!(
808            repo_path("x/y").strip_prefix(repo_path("x")),
809            Some(repo_path("y"))
810        );
811        assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
812        assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
813
814        assert_eq!(
815            repo_path("x/y").strip_prefix(repo_path("x/y")),
816            Some(repo_path(""))
817        );
818        assert_eq!(
819            repo_path("x/y/z").strip_prefix(repo_path("x/y")),
820            Some(repo_path("z"))
821        );
822        assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
823        assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
824        assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
825    }
826
827    #[test]
828    fn test_order() {
829        assert!(RepoPath::root() < repo_path("dir"));
830        assert!(repo_path("dir") < repo_path("dirx"));
831        // '#' < '/', but ["dir", "sub"] < ["dir#"]
832        assert!(repo_path("dir") < repo_path("dir#"));
833        assert!(repo_path("dir") < repo_path("dir/sub"));
834        assert!(repo_path("dir/sub") < repo_path("dir#"));
835
836        assert!(repo_path("abc") < repo_path("dir/file"));
837        assert!(repo_path("dir") < repo_path("dir/file"));
838        assert!(repo_path("dis") > repo_path("dir/file"));
839        assert!(repo_path("xyz") > repo_path("dir/file"));
840        assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
841    }
842
843    #[test]
844    fn test_join() {
845        let root = RepoPath::root();
846        let dir = root.join(repo_path_component("dir"));
847        assert_eq!(dir.as_ref(), repo_path("dir"));
848        let subdir = dir.join(repo_path_component("subdir"));
849        assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
850        assert_eq!(
851            subdir.join(repo_path_component("file")).as_ref(),
852            repo_path("dir/subdir/file")
853        );
854    }
855
856    #[test]
857    fn test_extend() {
858        let mut path = RepoPathBuf::root();
859        path.extend(std::iter::empty::<RepoPathComponentBuf>());
860        assert_eq!(path.as_ref(), RepoPath::root());
861        path.extend([repo_path_component("dir")]);
862        assert_eq!(path.as_ref(), repo_path("dir"));
863        path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
864        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
865        path.extend(std::iter::empty::<RepoPathComponentBuf>());
866        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
867    }
868
869    #[test]
870    fn test_parent() {
871        let root = RepoPath::root();
872        let dir_component = repo_path_component("dir");
873        let subdir_component = repo_path_component("subdir");
874
875        let dir = root.join(dir_component);
876        let subdir = dir.join(subdir_component);
877
878        assert_eq!(root.parent(), None);
879        assert_eq!(dir.parent(), Some(root));
880        assert_eq!(subdir.parent(), Some(dir.as_ref()));
881    }
882
883    #[test]
884    fn test_split() {
885        let root = RepoPath::root();
886        let dir_component = repo_path_component("dir");
887        let file_component = repo_path_component("file");
888
889        let dir = root.join(dir_component);
890        let file = dir.join(file_component);
891
892        assert_eq!(root.split(), None);
893        assert_eq!(dir.split(), Some((root, dir_component)));
894        assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
895    }
896
897    #[test]
898    fn test_components() {
899        assert!(RepoPath::root().components().next().is_none());
900        assert_eq!(
901            repo_path("dir").components().collect_vec(),
902            vec![repo_path_component("dir")]
903        );
904        assert_eq!(
905            repo_path("dir/subdir").components().collect_vec(),
906            vec![repo_path_component("dir"), repo_path_component("subdir")]
907        );
908
909        // Iterates from back
910        assert!(RepoPath::root().components().next_back().is_none());
911        assert_eq!(
912            repo_path("dir").components().rev().collect_vec(),
913            vec![repo_path_component("dir")]
914        );
915        assert_eq!(
916            repo_path("dir/subdir").components().rev().collect_vec(),
917            vec![repo_path_component("subdir"), repo_path_component("dir")]
918        );
919    }
920
921    #[test]
922    fn test_ancestors() {
923        assert_eq!(
924            RepoPath::root().ancestors().collect_vec(),
925            vec![RepoPath::root()]
926        );
927        assert_eq!(
928            repo_path("dir").ancestors().collect_vec(),
929            vec![repo_path("dir"), RepoPath::root()]
930        );
931        assert_eq!(
932            repo_path("dir/subdir").ancestors().collect_vec(),
933            vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
934        );
935    }
936
937    #[test]
938    fn test_to_fs_path() {
939        assert_eq!(
940            repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
941            Path::new("base/dir")
942        );
943        assert_eq!(
944            repo_path("").to_fs_path(Path::new("")).unwrap(),
945            Path::new(".")
946        );
947        assert_eq!(
948            repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
949            Path::new("base/dir/file")
950        );
951        assert_eq!(
952            repo_path("some/deep/dir/file")
953                .to_fs_path(Path::new("base/dir"))
954                .unwrap(),
955            Path::new("base/dir/some/deep/dir/file")
956        );
957        assert_eq!(
958            repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
959            Path::new("dir/file")
960        );
961
962        // Current/parent dir component
963        assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
964        assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
965        assert!(
966            repo_path("dir/../file")
967                .to_fs_path(Path::new("base"))
968                .is_err()
969        );
970        assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
971        assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
972        assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
973        assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
974
975        // Empty component (which is invalid as a repo path)
976        assert!(
977            RepoPath::from_internal_string_unchecked("/")
978                .to_fs_path(Path::new("base"))
979                .is_err()
980        );
981        assert_eq!(
982            // Iterator omits empty component after "/", which is fine so long
983            // as the returned path doesn't escape.
984            RepoPath::from_internal_string_unchecked("a/")
985                .to_fs_path(Path::new("base"))
986                .unwrap(),
987            Path::new("base/a")
988        );
989        assert!(
990            RepoPath::from_internal_string_unchecked("/b")
991                .to_fs_path(Path::new("base"))
992                .is_err()
993        );
994        assert!(
995            RepoPath::from_internal_string_unchecked("a//b")
996                .to_fs_path(Path::new("base"))
997                .is_err()
998        );
999
1000        // Component containing slash (simulating Windows path separator)
1001        assert!(
1002            RepoPathComponent::new_unchecked("wind/ows")
1003                .to_fs_name()
1004                .is_err()
1005        );
1006        assert!(
1007            RepoPathComponent::new_unchecked("./file")
1008                .to_fs_name()
1009                .is_err()
1010        );
1011        assert!(
1012            RepoPathComponent::new_unchecked("file/.")
1013                .to_fs_name()
1014                .is_err()
1015        );
1016        assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
1017
1018        // Windows path separator and drive letter
1019        if cfg!(windows) {
1020            assert!(
1021                repo_path(r#"wind\ows"#)
1022                    .to_fs_path(Path::new("base"))
1023                    .is_err()
1024            );
1025            assert!(
1026                repo_path(r#".\file"#)
1027                    .to_fs_path(Path::new("base"))
1028                    .is_err()
1029            );
1030            assert!(
1031                repo_path(r#"file\."#)
1032                    .to_fs_path(Path::new("base"))
1033                    .is_err()
1034            );
1035            assert!(
1036                repo_path(r#"c:/foo"#)
1037                    .to_fs_path(Path::new("base"))
1038                    .is_err()
1039            );
1040        }
1041    }
1042
1043    #[test]
1044    fn test_to_fs_path_unchecked() {
1045        assert_eq!(
1046            repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1047            Path::new("base/dir")
1048        );
1049        assert_eq!(
1050            repo_path("").to_fs_path_unchecked(Path::new("")),
1051            Path::new(".")
1052        );
1053        assert_eq!(
1054            repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1055            Path::new("base/dir/file")
1056        );
1057        assert_eq!(
1058            repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1059            Path::new("base/dir/some/deep/dir/file")
1060        );
1061        assert_eq!(
1062            repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1063            Path::new("dir/file")
1064        );
1065    }
1066
1067    #[test]
1068    fn test_split_common_prefix() {
1069        assert_eq!(
1070            repo_path("foo/bar").split_common_prefix(repo_path("foo/bar/baz")),
1071            (repo_path("foo/bar"), repo_path(""))
1072        );
1073
1074        assert_eq!(
1075            repo_path("foo/bar/baz").split_common_prefix(repo_path("foo/bar")),
1076            (repo_path("foo/bar"), repo_path("baz"))
1077        );
1078
1079        assert_eq!(
1080            repo_path("foo/bar/bing").split_common_prefix(repo_path("foo/bar/baz")),
1081            (repo_path("foo/bar"), repo_path("bing"))
1082        );
1083
1084        assert_eq!(
1085            repo_path("no/common/prefix").split_common_prefix(repo_path("foo/bar/baz")),
1086            (RepoPath::root(), repo_path("no/common/prefix"))
1087        );
1088
1089        assert_eq!(
1090            repo_path("same/path").split_common_prefix(repo_path("same/path")),
1091            (repo_path("same/path"), RepoPath::root())
1092        );
1093
1094        assert_eq!(
1095            RepoPath::root().split_common_prefix(repo_path("foo")),
1096            (RepoPath::root(), RepoPath::root())
1097        );
1098
1099        assert_eq!(
1100            RepoPath::root().split_common_prefix(RepoPath::root()),
1101            (RepoPath::root(), RepoPath::root())
1102        );
1103
1104        assert_eq!(
1105            repo_path("foo/bar").split_common_prefix(RepoPath::root()),
1106            (RepoPath::root(), repo_path("foo/bar"))
1107        );
1108    }
1109}