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, 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        RepoPathBuf {
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(RepoPathBuf { 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(RepoPathBuf { 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: &RepoPath) -> 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: &RepoPath) -> Option<&RepoPath> {
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(RepoPath::from_internal_string_unchecked(tail))
409            } else {
410                tail.strip_prefix('/')
411                    .map(RepoPath::from_internal_string_unchecked)
412            }
413        }
414    }
415
416    /// Returns the parent path without the base name component.
417    pub fn parent(&self) -> Option<&RepoPath> {
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<(&RepoPath, &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 = &RepoPath> {
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<RepoPath> for RepoPath {
447    fn as_ref(&self) -> &RepoPath {
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            RepoPathUiConverter::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            RepoPathUiConverter::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                    .rev()
644                    .zip(target_components.iter().rev())
645                    .take_while(|(source_component, target_component)| {
646                        source_component == target_component
647                    })
648                    .count()
649                    .min(source_components.len().saturating_sub(1))
650                    .min(target_components.len().saturating_sub(1));
651
652                fn format_components(c: &[std::path::Component]) -> String {
653                    c.iter().collect::<PathBuf>().display().to_string()
654                }
655
656                if prefix_count > 0 {
657                    formatted.push_str(&format_components(&source_components[0..prefix_count]));
658                    formatted.push_str(std::path::MAIN_SEPARATOR_STR);
659                }
660                formatted.push('{');
661                formatted.push_str(&format_components(
662                    &source_components
663                        [prefix_count..(source_components.len() - suffix_count).max(prefix_count)],
664                ));
665                formatted.push_str(" => ");
666                formatted.push_str(&format_components(
667                    &target_components
668                        [prefix_count..(target_components.len() - suffix_count).max(prefix_count)],
669                ));
670                formatted.push('}');
671                if suffix_count > 0 {
672                    formatted.push_str(std::path::MAIN_SEPARATOR_STR);
673                    formatted.push_str(&format_components(
674                        &source_components[source_components.len() - suffix_count..],
675                    ));
676                }
677            }
678        }
679        formatted
680    }
681
682    /// Parses a path from the UI.
683    ///
684    /// It's up to the implementation whether absolute paths are allowed, and
685    /// where relative paths are interpreted as relative to.
686    pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
687        match self {
688            RepoPathUiConverter::Fs { cwd, base } => {
689                RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
690            }
691        }
692    }
693}
694
695#[cfg(test)]
696mod tests {
697    use std::panic;
698
699    use assert_matches::assert_matches;
700    use itertools::Itertools as _;
701
702    use super::*;
703    use crate::tests::new_temp_dir;
704
705    fn repo_path(value: &str) -> &RepoPath {
706        RepoPath::from_internal_string(value).unwrap()
707    }
708
709    fn repo_path_component(value: &str) -> &RepoPathComponent {
710        RepoPathComponent::new(value).unwrap()
711    }
712
713    #[test]
714    fn test_is_root() {
715        assert!(RepoPath::root().is_root());
716        assert!(repo_path("").is_root());
717        assert!(!repo_path("foo").is_root());
718    }
719
720    #[test]
721    fn test_from_internal_string() {
722        let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
723        assert_eq!(repo_path_buf(""), RepoPathBuf::root());
724        assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
725        assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
726        assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
727        assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
728
729        assert_eq!(repo_path(""), RepoPath::root());
730        assert!(panic::catch_unwind(|| repo_path("/")).is_err());
731        assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
732        assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
733        assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
734    }
735
736    #[test]
737    fn test_as_internal_file_string() {
738        assert_eq!(RepoPath::root().as_internal_file_string(), "");
739        assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
740        assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
741    }
742
743    #[test]
744    fn test_to_internal_dir_string() {
745        assert_eq!(RepoPath::root().to_internal_dir_string(), "");
746        assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
747        assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
748    }
749
750    #[test]
751    fn test_starts_with() {
752        assert!(repo_path("").starts_with(repo_path("")));
753        assert!(repo_path("x").starts_with(repo_path("")));
754        assert!(!repo_path("").starts_with(repo_path("x")));
755
756        assert!(repo_path("x").starts_with(repo_path("x")));
757        assert!(repo_path("x/y").starts_with(repo_path("x")));
758        assert!(!repo_path("xy").starts_with(repo_path("x")));
759        assert!(!repo_path("x/y").starts_with(repo_path("y")));
760
761        assert!(repo_path("x/y").starts_with(repo_path("x/y")));
762        assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
763        assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
764        assert!(!repo_path("x").starts_with(repo_path("x/y")));
765        assert!(!repo_path("xy").starts_with(repo_path("x/y")));
766    }
767
768    #[test]
769    fn test_strip_prefix() {
770        assert_eq!(
771            repo_path("").strip_prefix(repo_path("")),
772            Some(repo_path(""))
773        );
774        assert_eq!(
775            repo_path("x").strip_prefix(repo_path("")),
776            Some(repo_path("x"))
777        );
778        assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
779
780        assert_eq!(
781            repo_path("x").strip_prefix(repo_path("x")),
782            Some(repo_path(""))
783        );
784        assert_eq!(
785            repo_path("x/y").strip_prefix(repo_path("x")),
786            Some(repo_path("y"))
787        );
788        assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
789        assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
790
791        assert_eq!(
792            repo_path("x/y").strip_prefix(repo_path("x/y")),
793            Some(repo_path(""))
794        );
795        assert_eq!(
796            repo_path("x/y/z").strip_prefix(repo_path("x/y")),
797            Some(repo_path("z"))
798        );
799        assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
800        assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
801        assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
802    }
803
804    #[test]
805    fn test_order() {
806        assert!(RepoPath::root() < repo_path("dir"));
807        assert!(repo_path("dir") < repo_path("dirx"));
808        // '#' < '/', but ["dir", "sub"] < ["dir#"]
809        assert!(repo_path("dir") < repo_path("dir#"));
810        assert!(repo_path("dir") < repo_path("dir/sub"));
811        assert!(repo_path("dir/sub") < repo_path("dir#"));
812
813        assert!(repo_path("abc") < repo_path("dir/file"));
814        assert!(repo_path("dir") < repo_path("dir/file"));
815        assert!(repo_path("dis") > repo_path("dir/file"));
816        assert!(repo_path("xyz") > repo_path("dir/file"));
817        assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
818    }
819
820    #[test]
821    fn test_join() {
822        let root = RepoPath::root();
823        let dir = root.join(repo_path_component("dir"));
824        assert_eq!(dir.as_ref(), repo_path("dir"));
825        let subdir = dir.join(repo_path_component("subdir"));
826        assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
827        assert_eq!(
828            subdir.join(repo_path_component("file")).as_ref(),
829            repo_path("dir/subdir/file")
830        );
831    }
832
833    #[test]
834    fn test_extend() {
835        let mut path = RepoPathBuf::root();
836        path.extend(std::iter::empty::<RepoPathComponentBuf>());
837        assert_eq!(path.as_ref(), RepoPath::root());
838        path.extend([repo_path_component("dir")]);
839        assert_eq!(path.as_ref(), repo_path("dir"));
840        path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
841        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
842        path.extend(std::iter::empty::<RepoPathComponentBuf>());
843        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
844    }
845
846    #[test]
847    fn test_parent() {
848        let root = RepoPath::root();
849        let dir_component = repo_path_component("dir");
850        let subdir_component = repo_path_component("subdir");
851
852        let dir = root.join(dir_component);
853        let subdir = dir.join(subdir_component);
854
855        assert_eq!(root.parent(), None);
856        assert_eq!(dir.parent(), Some(root));
857        assert_eq!(subdir.parent(), Some(dir.as_ref()));
858    }
859
860    #[test]
861    fn test_split() {
862        let root = RepoPath::root();
863        let dir_component = repo_path_component("dir");
864        let file_component = repo_path_component("file");
865
866        let dir = root.join(dir_component);
867        let file = dir.join(file_component);
868
869        assert_eq!(root.split(), None);
870        assert_eq!(dir.split(), Some((root, dir_component)));
871        assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
872    }
873
874    #[test]
875    fn test_components() {
876        assert!(RepoPath::root().components().next().is_none());
877        assert_eq!(
878            repo_path("dir").components().collect_vec(),
879            vec![repo_path_component("dir")]
880        );
881        assert_eq!(
882            repo_path("dir/subdir").components().collect_vec(),
883            vec![repo_path_component("dir"), repo_path_component("subdir")]
884        );
885
886        // Iterates from back
887        assert!(RepoPath::root().components().next_back().is_none());
888        assert_eq!(
889            repo_path("dir").components().rev().collect_vec(),
890            vec![repo_path_component("dir")]
891        );
892        assert_eq!(
893            repo_path("dir/subdir").components().rev().collect_vec(),
894            vec![repo_path_component("subdir"), repo_path_component("dir")]
895        );
896    }
897
898    #[test]
899    fn test_ancestors() {
900        assert_eq!(
901            RepoPath::root().ancestors().collect_vec(),
902            vec![RepoPath::root()]
903        );
904        assert_eq!(
905            repo_path("dir").ancestors().collect_vec(),
906            vec![repo_path("dir"), RepoPath::root()]
907        );
908        assert_eq!(
909            repo_path("dir/subdir").ancestors().collect_vec(),
910            vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
911        );
912    }
913
914    #[test]
915    fn test_to_fs_path() {
916        assert_eq!(
917            repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
918            Path::new("base/dir")
919        );
920        assert_eq!(
921            repo_path("").to_fs_path(Path::new("")).unwrap(),
922            Path::new(".")
923        );
924        assert_eq!(
925            repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
926            Path::new("base/dir/file")
927        );
928        assert_eq!(
929            repo_path("some/deep/dir/file")
930                .to_fs_path(Path::new("base/dir"))
931                .unwrap(),
932            Path::new("base/dir/some/deep/dir/file")
933        );
934        assert_eq!(
935            repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
936            Path::new("dir/file")
937        );
938
939        // Current/parent dir component
940        assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
941        assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
942        assert!(repo_path("dir/../file")
943            .to_fs_path(Path::new("base"))
944            .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        assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
949
950        // Empty component (which is invalid as a repo path)
951        assert!(RepoPath::from_internal_string_unchecked("/")
952            .to_fs_path(Path::new("base"))
953            .is_err());
954        assert_eq!(
955            // Iterator omits empty component after "/", which is fine so long
956            // as the returned path doesn't escape.
957            RepoPath::from_internal_string_unchecked("a/")
958                .to_fs_path(Path::new("base"))
959                .unwrap(),
960            Path::new("base/a")
961        );
962        assert!(RepoPath::from_internal_string_unchecked("/b")
963            .to_fs_path(Path::new("base"))
964            .is_err());
965        assert!(RepoPath::from_internal_string_unchecked("a//b")
966            .to_fs_path(Path::new("base"))
967            .is_err());
968
969        // Component containing slash (simulating Windows path separator)
970        assert!(RepoPathComponent::new_unchecked("wind/ows")
971            .to_fs_name()
972            .is_err());
973        assert!(RepoPathComponent::new_unchecked("./file")
974            .to_fs_name()
975            .is_err());
976        assert!(RepoPathComponent::new_unchecked("file/.")
977            .to_fs_name()
978            .is_err());
979        assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
980
981        // Windows path separator and drive letter
982        if cfg!(windows) {
983            assert!(repo_path(r#"wind\ows"#)
984                .to_fs_path(Path::new("base"))
985                .is_err());
986            assert!(repo_path(r#".\file"#)
987                .to_fs_path(Path::new("base"))
988                .is_err());
989            assert!(repo_path(r#"file\."#)
990                .to_fs_path(Path::new("base"))
991                .is_err());
992            assert!(repo_path(r#"c:/foo"#)
993                .to_fs_path(Path::new("base"))
994                .is_err());
995        }
996    }
997
998    #[test]
999    fn test_to_fs_path_unchecked() {
1000        assert_eq!(
1001            repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1002            Path::new("base/dir")
1003        );
1004        assert_eq!(
1005            repo_path("").to_fs_path_unchecked(Path::new("")),
1006            Path::new(".")
1007        );
1008        assert_eq!(
1009            repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1010            Path::new("base/dir/file")
1011        );
1012        assert_eq!(
1013            repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1014            Path::new("base/dir/some/deep/dir/file")
1015        );
1016        assert_eq!(
1017            repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1018            Path::new("dir/file")
1019        );
1020    }
1021
1022    #[test]
1023    fn parse_fs_path_wc_in_cwd() {
1024        let temp_dir = new_temp_dir();
1025        let cwd_path = temp_dir.path().join("repo");
1026        let wc_path = &cwd_path;
1027
1028        assert_eq!(
1029            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1030            Ok(RepoPath::root())
1031        );
1032        assert_eq!(
1033            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1034            Ok(RepoPath::root())
1035        );
1036        assert_eq!(
1037            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1038            Ok(repo_path("file"))
1039        );
1040        // Both slash and the platform's separator are allowed
1041        assert_eq!(
1042            RepoPathBuf::parse_fs_path(
1043                &cwd_path,
1044                wc_path,
1045                format!("dir{}file", std::path::MAIN_SEPARATOR)
1046            )
1047            .as_deref(),
1048            Ok(repo_path("dir/file"))
1049        );
1050        assert_eq!(
1051            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1052            Ok(repo_path("dir/file"))
1053        );
1054        assert_matches!(
1055            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1056            Err(FsPathParseError {
1057                source: RelativePathParseError::InvalidComponent { .. },
1058                ..
1059            })
1060        );
1061        assert_eq!(
1062            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1063            Ok(RepoPath::root())
1064        );
1065        assert_eq!(
1066            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1067            Ok(repo_path("file"))
1068        );
1069        // Input may be absolute path with ".."
1070        assert_eq!(
1071            RepoPathBuf::parse_fs_path(
1072                &cwd_path,
1073                &cwd_path,
1074                cwd_path.join("../repo").to_str().unwrap()
1075            )
1076            .as_deref(),
1077            Ok(RepoPath::root())
1078        );
1079    }
1080
1081    #[test]
1082    fn parse_fs_path_wc_in_cwd_parent() {
1083        let temp_dir = new_temp_dir();
1084        let cwd_path = temp_dir.path().join("dir");
1085        let wc_path = cwd_path.parent().unwrap().to_path_buf();
1086
1087        assert_eq!(
1088            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1089            Ok(repo_path("dir"))
1090        );
1091        assert_eq!(
1092            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1093            Ok(repo_path("dir"))
1094        );
1095        assert_eq!(
1096            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1097            Ok(repo_path("dir/file"))
1098        );
1099        assert_eq!(
1100            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1101            Ok(repo_path("dir/subdir/file"))
1102        );
1103        assert_eq!(
1104            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1105            Ok(RepoPath::root())
1106        );
1107        assert_matches!(
1108            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1109            Err(FsPathParseError {
1110                source: RelativePathParseError::InvalidComponent { .. },
1111                ..
1112            })
1113        );
1114        assert_eq!(
1115            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1116            Ok(repo_path("other-dir/file"))
1117        );
1118    }
1119
1120    #[test]
1121    fn parse_fs_path_wc_in_cwd_child() {
1122        let temp_dir = new_temp_dir();
1123        let cwd_path = temp_dir.path().join("cwd");
1124        let wc_path = cwd_path.join("repo");
1125
1126        assert_matches!(
1127            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1128            Err(FsPathParseError {
1129                source: RelativePathParseError::InvalidComponent { .. },
1130                ..
1131            })
1132        );
1133        assert_matches!(
1134            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1135            Err(FsPathParseError {
1136                source: RelativePathParseError::InvalidComponent { .. },
1137                ..
1138            })
1139        );
1140        assert_eq!(
1141            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1142            Ok(RepoPath::root())
1143        );
1144        assert_eq!(
1145            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1146            Ok(repo_path("file"))
1147        );
1148        assert_eq!(
1149            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1150            Ok(repo_path("dir/file"))
1151        );
1152    }
1153
1154    #[test]
1155    fn test_format_copied_path() {
1156        let ui = RepoPathUiConverter::Fs {
1157            cwd: PathBuf::from("."),
1158            base: PathBuf::from("."),
1159        };
1160
1161        let format = |before, after| {
1162            ui.format_copied_path(repo_path(before), repo_path(after))
1163                .replace('\\', "/")
1164        };
1165
1166        assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1167        assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1168        assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1169        assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1170        assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1171        assert_eq!(
1172            format("one/two", "one/two/three/one/two"),
1173            "one/{ => two/three/one}/two"
1174        );
1175
1176        assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1177        assert_eq!(
1178            format("one/two/three", "one/four/three"),
1179            "one/{two => four}/three"
1180        );
1181        assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1182        assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1183        assert_eq!(format("two", "four"), "{two => four}");
1184        assert_eq!(format("file1", "file2"), "{file1 => file2}");
1185        assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1186    }
1187}