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