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#![expect(missing_docs)]
16
17use std::borrow::Borrow;
18use std::cmp::Ordering;
19use std::collections::HashMap;
20use std::fmt;
21use std::fmt::Debug;
22use std::fmt::Formatter;
23use std::iter;
24use std::iter::FusedIterator;
25use std::ops::Deref;
26use std::path::Component;
27use std::path::Path;
28use std::path::PathBuf;
29
30use itertools::Itertools as _;
31use ref_cast::RefCastCustom;
32use ref_cast::ref_cast_custom;
33use thiserror::Error;
34
35use crate::content_hash::ContentHash;
36use crate::file_util;
37use crate::merge::Diff;
38
39/// Owned `RepoPath` component.
40#[derive(ContentHash, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub struct RepoPathComponentBuf {
42    // Don't add more fields. Eq, Hash, and Ord must be compatible with the
43    // borrowed RepoPathComponent type.
44    value: String,
45}
46
47impl RepoPathComponentBuf {
48    /// Wraps `value` as `RepoPathComponentBuf`.
49    ///
50    /// Returns an error if the input `value` is empty or contains path
51    /// separator.
52    pub fn new(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
53        let value: String = value.into();
54        if is_valid_repo_path_component_str(&value) {
55            Ok(Self { value })
56        } else {
57            Err(InvalidNewRepoPathError { value })
58        }
59    }
60}
61
62/// Borrowed `RepoPath` component.
63#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, RefCastCustom)]
64#[repr(transparent)]
65pub struct RepoPathComponent {
66    value: str,
67}
68
69impl RepoPathComponent {
70    /// Wraps `value` as `RepoPathComponent`.
71    ///
72    /// Returns an error if the input `value` is empty or contains path
73    /// separator.
74    pub fn new(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
75        if is_valid_repo_path_component_str(value) {
76            Ok(Self::new_unchecked(value))
77        } else {
78            Err(InvalidNewRepoPathError {
79                value: value.to_string(),
80            })
81        }
82    }
83
84    #[ref_cast_custom]
85    const fn new_unchecked(value: &str) -> &Self;
86
87    /// Returns the underlying string representation.
88    pub fn as_internal_str(&self) -> &str {
89        &self.value
90    }
91
92    /// Returns a normal filesystem entry name if this path component is valid
93    /// as a file/directory name.
94    pub fn to_fs_name(&self) -> Result<&str, InvalidRepoPathComponentError> {
95        let mut components = Path::new(&self.value).components().fuse();
96        match (components.next(), components.next()) {
97            // Trailing "." can be normalized by Path::components(), so compare
98            // component name. e.g. "foo\." (on Windows) should be rejected.
99            (Some(Component::Normal(name)), None) if name == &self.value => Ok(&self.value),
100            // e.g. ".", "..", "foo\bar" (on Windows)
101            _ => Err(InvalidRepoPathComponentError {
102                component: self.value.into(),
103            }),
104        }
105    }
106}
107
108impl Debug for RepoPathComponent {
109    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
110        write!(f, "{:?}", &self.value)
111    }
112}
113
114impl Debug for RepoPathComponentBuf {
115    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
116        <RepoPathComponent as Debug>::fmt(self, f)
117    }
118}
119
120impl AsRef<Self> for RepoPathComponent {
121    fn as_ref(&self) -> &Self {
122        self
123    }
124}
125
126impl AsRef<RepoPathComponent> for RepoPathComponentBuf {
127    fn as_ref(&self) -> &RepoPathComponent {
128        self
129    }
130}
131
132impl Borrow<RepoPathComponent> for RepoPathComponentBuf {
133    fn borrow(&self) -> &RepoPathComponent {
134        self
135    }
136}
137
138impl Deref for RepoPathComponentBuf {
139    type Target = RepoPathComponent;
140
141    fn deref(&self) -> &Self::Target {
142        RepoPathComponent::new_unchecked(&self.value)
143    }
144}
145
146impl ToOwned for RepoPathComponent {
147    type Owned = RepoPathComponentBuf;
148
149    fn to_owned(&self) -> Self::Owned {
150        let value = self.value.to_owned();
151        RepoPathComponentBuf { value }
152    }
153
154    fn clone_into(&self, target: &mut Self::Owned) {
155        self.value.clone_into(&mut target.value);
156    }
157}
158
159/// Iterator over `RepoPath` components.
160#[derive(Clone, Debug)]
161pub struct RepoPathComponentsIter<'a> {
162    value: &'a str,
163}
164
165impl<'a> RepoPathComponentsIter<'a> {
166    /// Returns the remaining part as repository path.
167    pub fn as_path(&self) -> &'a RepoPath {
168        RepoPath::from_internal_string_unchecked(self.value)
169    }
170}
171
172impl<'a> Iterator for RepoPathComponentsIter<'a> {
173    type Item = &'a RepoPathComponent;
174
175    fn next(&mut self) -> Option<Self::Item> {
176        if self.value.is_empty() {
177            return None;
178        }
179        let (name, remainder) = self
180            .value
181            .split_once('/')
182            .unwrap_or_else(|| (self.value, &self.value[self.value.len()..]));
183        self.value = remainder;
184        Some(RepoPathComponent::new_unchecked(name))
185    }
186}
187
188impl DoubleEndedIterator for RepoPathComponentsIter<'_> {
189    fn next_back(&mut self) -> Option<Self::Item> {
190        if self.value.is_empty() {
191            return None;
192        }
193        let (remainder, name) = self
194            .value
195            .rsplit_once('/')
196            .unwrap_or_else(|| (&self.value[..0], self.value));
197        self.value = remainder;
198        Some(RepoPathComponent::new_unchecked(name))
199    }
200}
201
202impl FusedIterator for RepoPathComponentsIter<'_> {}
203
204/// Owned repository path.
205#[derive(ContentHash, Clone, Eq, Hash, PartialEq, serde::Serialize)]
206#[serde(transparent)]
207pub struct RepoPathBuf {
208    // Don't add more fields. Eq, Hash, and Ord must be compatible with the
209    // borrowed RepoPath type.
210    value: String,
211}
212
213/// Borrowed repository path.
214#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom, serde::Serialize)]
215#[repr(transparent)]
216#[serde(transparent)]
217pub struct RepoPath {
218    value: str,
219}
220
221impl Debug for RepoPath {
222    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
223        write!(f, "{:?}", &self.value)
224    }
225}
226
227impl Debug for RepoPathBuf {
228    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
229        <RepoPath as Debug>::fmt(self, f)
230    }
231}
232
233/// The `value` is not a valid repo path because it contains empty path
234/// component. For example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all
235/// invalid.
236#[derive(Clone, Debug, Eq, Error, PartialEq)]
237#[error(r#"Invalid repo path input "{value}""#)]
238pub struct InvalidNewRepoPathError {
239    value: String,
240}
241
242impl RepoPathBuf {
243    /// Creates owned repository path pointing to the root.
244    pub const fn root() -> Self {
245        Self {
246            value: String::new(),
247        }
248    }
249
250    /// Creates `RepoPathBuf` from valid string representation.
251    pub fn from_internal_string(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
252        let value: String = value.into();
253        if is_valid_repo_path_str(&value) {
254            Ok(Self { value })
255        } else {
256            Err(InvalidNewRepoPathError { value })
257        }
258    }
259
260    /// Converts repo-relative `Path` to `RepoPathBuf`.
261    ///
262    /// The input path should not contain redundant `.` or `..`.
263    pub fn from_relative_path(
264        relative_path: impl AsRef<Path>,
265    ) -> Result<Self, RelativePathParseError> {
266        let relative_path = relative_path.as_ref();
267        if relative_path == Path::new(".") {
268            return Ok(Self::root());
269        }
270
271        let mut components = relative_path
272            .components()
273            .map(|c| match c {
274                Component::Normal(name) => {
275                    name.to_str()
276                        .ok_or_else(|| RelativePathParseError::InvalidUtf8 {
277                            path: relative_path.into(),
278                        })
279                }
280                _ => Err(RelativePathParseError::InvalidComponent {
281                    component: c.as_os_str().to_string_lossy().into(),
282                    path: relative_path.into(),
283                }),
284            })
285            .fuse();
286        let mut value = String::with_capacity(relative_path.as_os_str().len());
287        if let Some(name) = components.next() {
288            value.push_str(name?);
289        }
290        for name in components {
291            value.push('/');
292            value.push_str(name?);
293        }
294        Ok(Self { value })
295    }
296
297    /// Parses an `input` path into a `RepoPathBuf` relative to `base`.
298    ///
299    /// The `cwd` and `base` paths are supposed to be absolute and normalized in
300    /// the same manner. The `input` path may be either relative to `cwd` or
301    /// absolute.
302    pub fn parse_fs_path(
303        cwd: &Path,
304        base: &Path,
305        input: impl AsRef<Path>,
306    ) -> Result<Self, FsPathParseError> {
307        let input = input.as_ref();
308        let abs_input_path = file_util::normalize_path(&cwd.join(input));
309        let repo_relative_path = file_util::relative_path(base, &abs_input_path);
310        Self::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError {
311            base: file_util::relative_path(cwd, base).into(),
312            input: input.into(),
313            source,
314        })
315    }
316
317    /// Consumes this and returns the underlying string representation.
318    pub fn into_internal_string(self) -> String {
319        self.value
320    }
321}
322
323impl RepoPath {
324    /// Returns repository path pointing to the root.
325    pub const fn root() -> &'static Self {
326        Self::from_internal_string_unchecked("")
327    }
328
329    /// Wraps valid string representation as `RepoPath`.
330    ///
331    /// Returns an error if the input `value` contains empty path component. For
332    /// example, `"/"`, `"/foo"`, `"foo/"`, `"foo//bar"` are all invalid.
333    pub fn from_internal_string(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
334        if is_valid_repo_path_str(value) {
335            Ok(Self::from_internal_string_unchecked(value))
336        } else {
337            Err(InvalidNewRepoPathError {
338                value: value.to_owned(),
339            })
340        }
341    }
342
343    #[ref_cast_custom]
344    const fn from_internal_string_unchecked(value: &str) -> &Self;
345
346    /// The full string form used internally, not for presenting to users (where
347    /// we may want to use the platform's separator). This format includes a
348    /// trailing slash, unless this path represents the root directory. That
349    /// way it can be concatenated with a basename and produce a valid path.
350    pub fn to_internal_dir_string(&self) -> String {
351        if self.value.is_empty() {
352            String::new()
353        } else {
354            [&self.value, "/"].concat()
355        }
356    }
357
358    /// The full string form used internally, not for presenting to users (where
359    /// we may want to use the platform's separator).
360    pub fn as_internal_file_string(&self) -> &str {
361        &self.value
362    }
363
364    /// Converts repository path to filesystem path relative to the `base`.
365    ///
366    /// The returned path should never contain `..`, `C:` (on Windows), etc.
367    /// However, it may contain reserved working-copy directories such as `.jj`.
368    pub fn to_fs_path(&self, base: &Path) -> Result<PathBuf, InvalidRepoPathError> {
369        let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
370        result.push(base);
371        for c in self.components() {
372            result.push(c.to_fs_name().map_err(|err| err.with_path(self))?);
373        }
374        if result.as_os_str().is_empty() {
375            result.push(".");
376        }
377        Ok(result)
378    }
379
380    /// Converts repository path to filesystem path relative to the `base`,
381    /// without checking invalid path components.
382    ///
383    /// The returned path may point outside of the `base` directory. Use this
384    /// function only for displaying or testing purposes.
385    pub fn to_fs_path_unchecked(&self, base: &Path) -> PathBuf {
386        let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
387        result.push(base);
388        result.extend(self.components().map(RepoPathComponent::as_internal_str));
389        if result.as_os_str().is_empty() {
390            result.push(".");
391        }
392        result
393    }
394
395    pub fn is_root(&self) -> bool {
396        self.value.is_empty()
397    }
398
399    /// Returns true if the `base` is a prefix of this path.
400    pub fn starts_with(&self, base: &Self) -> bool {
401        self.strip_prefix(base).is_some()
402    }
403
404    /// Returns the remaining path with the `base` path removed.
405    pub fn strip_prefix(&self, base: &Self) -> Option<&Self> {
406        if base.value.is_empty() {
407            Some(self)
408        } else {
409            let tail = self.value.strip_prefix(&base.value)?;
410            if tail.is_empty() {
411                Some(Self::from_internal_string_unchecked(tail))
412            } else {
413                tail.strip_prefix('/')
414                    .map(Self::from_internal_string_unchecked)
415            }
416        }
417    }
418
419    /// Returns the parent path without the base name component.
420    pub fn parent(&self) -> Option<&Self> {
421        self.split().map(|(parent, _)| parent)
422    }
423
424    /// Splits this into the parent path and base name component.
425    pub fn split(&self) -> Option<(&Self, &RepoPathComponent)> {
426        let mut components = self.components();
427        let basename = components.next_back()?;
428        Some((components.as_path(), basename))
429    }
430
431    pub fn components(&self) -> RepoPathComponentsIter<'_> {
432        RepoPathComponentsIter { value: &self.value }
433    }
434
435    pub fn ancestors(&self) -> impl Iterator<Item = &Self> {
436        std::iter::successors(Some(self), |path| path.parent())
437    }
438
439    pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf {
440        let value = if self.value.is_empty() {
441            entry.as_internal_str().to_owned()
442        } else {
443            [&self.value, "/", entry.as_internal_str()].concat()
444        };
445        RepoPathBuf { value }
446    }
447
448    /// Splits this path at its common prefix with `other`.
449    ///
450    /// # Returns
451    ///
452    /// Returns the `(common_prefix, self_remainder)`.
453    ///
454    /// All paths will at least have `RepoPath::root()` as a common prefix,
455    /// therefore even if `self` and `other` have no matching parent component
456    /// this function will always return at least `(RepoPath::root(), self)`.
457    ///
458    ///
459    /// # Examples
460    ///
461    /// ```
462    /// use jj_lib::repo_path::RepoPath;
463    ///
464    /// let bing_path = RepoPath::from_internal_string("foo/bar/bing").unwrap();
465    ///
466    /// let baz_path = RepoPath::from_internal_string("foo/bar/baz").unwrap();
467    ///
468    /// let foo_bar_path = RepoPath::from_internal_string("foo/bar").unwrap();
469    ///
470    /// assert_eq!(
471    ///     bing_path.split_common_prefix(&baz_path),
472    ///     (foo_bar_path, RepoPath::from_internal_string("bing").unwrap())
473    /// );
474    ///
475    /// let unrelated_path = RepoPath::from_internal_string("no/common/prefix").unwrap();
476    /// assert_eq!(
477    ///     baz_path.split_common_prefix(&unrelated_path),
478    ///     (RepoPath::root(), baz_path)
479    /// );
480    /// ```
481    pub fn split_common_prefix(&self, other: &Self) -> (&Self, &Self) {
482        // Obtain the common prefix between these paths
483        let mut prefix_len = 0;
484
485        let common_components = self
486            .components()
487            .zip(other.components())
488            .take_while(|(prev_comp, this_comp)| prev_comp == this_comp);
489
490        for (self_comp, _other_comp) in common_components {
491            if prefix_len > 0 {
492                // + 1 for all paths to take their separators into account.
493                // We skip the first one since there are ComponentCount - 1 separators in a
494                // path.
495                prefix_len += 1;
496            }
497
498            prefix_len += self_comp.value.len();
499        }
500
501        if prefix_len == 0 {
502            // No common prefix except root
503            return (Self::root(), self);
504        }
505
506        if prefix_len == self.value.len() {
507            return (self, Self::root());
508        }
509
510        let common_prefix = Self::from_internal_string_unchecked(&self.value[..prefix_len]);
511        let remainder = Self::from_internal_string_unchecked(&self.value[prefix_len + 1..]);
512
513        (common_prefix, remainder)
514    }
515}
516
517impl AsRef<Self> for RepoPath {
518    fn as_ref(&self) -> &Self {
519        self
520    }
521}
522
523impl AsRef<RepoPath> for RepoPathBuf {
524    fn as_ref(&self) -> &RepoPath {
525        self
526    }
527}
528
529impl Borrow<RepoPath> for RepoPathBuf {
530    fn borrow(&self) -> &RepoPath {
531        self
532    }
533}
534
535impl Deref for RepoPathBuf {
536    type Target = RepoPath;
537
538    fn deref(&self) -> &Self::Target {
539        RepoPath::from_internal_string_unchecked(&self.value)
540    }
541}
542
543impl ToOwned for RepoPath {
544    type Owned = RepoPathBuf;
545
546    fn to_owned(&self) -> Self::Owned {
547        let value = self.value.to_owned();
548        RepoPathBuf { value }
549    }
550
551    fn clone_into(&self, target: &mut Self::Owned) {
552        self.value.clone_into(&mut target.value);
553    }
554}
555
556impl Ord for RepoPath {
557    fn cmp(&self, other: &Self) -> Ordering {
558        // If there were leading/trailing slash, components-based Ord would
559        // disagree with str-based Eq.
560        debug_assert!(is_valid_repo_path_str(&self.value));
561        self.components().cmp(other.components())
562    }
563}
564
565impl Ord for RepoPathBuf {
566    fn cmp(&self, other: &Self) -> Ordering {
567        <RepoPath as Ord>::cmp(self, other)
568    }
569}
570
571impl PartialOrd for RepoPath {
572    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
573        Some(self.cmp(other))
574    }
575}
576
577impl PartialOrd for RepoPathBuf {
578    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
579        Some(self.cmp(other))
580    }
581}
582
583impl<P: AsRef<RepoPathComponent>> Extend<P> for RepoPathBuf {
584    fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
585        for component in iter {
586            if !self.value.is_empty() {
587                self.value.push('/');
588            }
589            self.value.push_str(component.as_ref().as_internal_str());
590        }
591    }
592}
593
594/// `RepoPath` contained invalid file/directory component such as `..`.
595#[derive(Clone, Debug, Eq, Error, PartialEq)]
596#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())]
597pub struct InvalidRepoPathError {
598    /// Path containing an error.
599    pub path: RepoPathBuf,
600    /// Source error.
601    pub source: InvalidRepoPathComponentError,
602}
603
604/// `RepoPath` component was invalid. (e.g. `..`)
605#[derive(Clone, Debug, Eq, Error, PartialEq)]
606#[error(r#"Invalid path component "{component}""#)]
607pub struct InvalidRepoPathComponentError {
608    pub component: Box<str>,
609}
610
611impl InvalidRepoPathComponentError {
612    /// Attaches the `path` that caused the error.
613    pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError {
614        InvalidRepoPathError {
615            path: path.to_owned(),
616            source: self,
617        }
618    }
619}
620
621#[derive(Clone, Debug, Eq, Error, PartialEq)]
622pub enum RelativePathParseError {
623    #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
624    InvalidComponent {
625        component: Box<str>,
626        path: Box<Path>,
627    },
628    #[error(r#"Not valid UTF-8 path "{path}""#)]
629    InvalidUtf8 { path: Box<Path> },
630}
631
632#[derive(Clone, Debug, Eq, Error, PartialEq)]
633#[error(r#"Path "{input}" is not in the repo "{base}""#)]
634pub struct FsPathParseError {
635    /// Repository or workspace root path relative to the `cwd`.
636    pub base: Box<Path>,
637    /// Input path without normalization.
638    pub input: Box<Path>,
639    /// Source error.
640    pub source: RelativePathParseError,
641}
642
643fn is_valid_repo_path_component_str(value: &str) -> bool {
644    !value.is_empty() && !value.contains('/')
645}
646
647fn is_valid_repo_path_str(value: &str) -> bool {
648    !value.starts_with('/') && !value.ends_with('/') && !value.contains("//")
649}
650
651/// An error from `RepoPathUiConverter::parse_file_path`.
652#[derive(Debug, Error)]
653pub enum UiPathParseError {
654    #[error(transparent)]
655    Fs(FsPathParseError),
656}
657
658/// Converts `RepoPath`s to and from plain strings as displayed to the user
659/// (e.g. relative to CWD).
660#[derive(Debug, Clone)]
661pub enum RepoPathUiConverter {
662    /// Variant for a local file system. Paths are interpreted relative to `cwd`
663    /// with the repo rooted in `base`.
664    ///
665    /// The `cwd` and `base` paths are supposed to be absolute and normalized in
666    /// the same manner.
667    Fs { cwd: PathBuf, base: PathBuf },
668    // TODO: Add a no-op variant that uses the internal `RepoPath` representation. Can be useful
669    // on a server.
670}
671
672impl RepoPathUiConverter {
673    /// Format a path for display in the UI.
674    pub fn format_file_path(&self, file: &RepoPath) -> String {
675        match self {
676            Self::Fs { cwd, base } => {
677                file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
678                    .display()
679                    .to_string()
680            }
681        }
682    }
683
684    /// Format a copy from `before` to `after` for display in the UI by
685    /// extracting common components and producing something like
686    /// "common/prefix/{before => after}/common/suffix".
687    ///
688    /// If `before == after`, this is equivalent to `format_file_path()`.
689    pub fn format_copied_path(&self, paths: Diff<&RepoPath>) -> String {
690        match self {
691            Self::Fs { .. } => {
692                let paths = paths.map(|path| self.format_file_path(path));
693                collapse_copied_path(paths.as_deref(), std::path::MAIN_SEPARATOR)
694            }
695        }
696    }
697
698    /// Parses a path from the UI.
699    ///
700    /// It's up to the implementation whether absolute paths are allowed, and
701    /// where relative paths are interpreted as relative to.
702    pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
703        match self {
704            Self::Fs { cwd, base } => {
705                RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
706            }
707        }
708    }
709}
710
711fn collapse_copied_path(paths: Diff<&str>, separator: char) -> String {
712    // The last component should never match middle components. This is ensured
713    // by including trailing separators. e.g. ("a/b", "a/b/x") => ("a/", _)
714    let components = paths.map(|path| path.split_inclusive(separator));
715    let prefix_len: usize = iter::zip(components.before, components.after)
716        .take_while(|(before, after)| before == after)
717        .map(|(_, after)| after.len())
718        .sum();
719    if paths.before.len() == prefix_len && paths.after.len() == prefix_len {
720        return paths.after.to_owned();
721    }
722
723    // The first component should never match middle components, but the first
724    // uncommon middle component can. e.g. ("a/b", "x/a/b") => ("", "/b"),
725    // ("a/b", "a/x/b") => ("a/", "/b")
726    let components = paths.map(|path| {
727        let mut remainder = &path[prefix_len.saturating_sub(1)..];
728        iter::from_fn(move || {
729            let pos = remainder.rfind(separator)?;
730            let (prefix, last) = remainder.split_at(pos);
731            remainder = prefix;
732            Some(last)
733        })
734    });
735    let suffix_len: usize = iter::zip(components.before, components.after)
736        .take_while(|(before, after)| before == after)
737        .map(|(_, after)| after.len())
738        .sum();
739
740    // Middle range may be invalid (start > end) because the same separator char
741    // can be distributed to both common prefix and suffix. e.g.
742    // ("a/b", "a/x/b") == ("a//b", "a/x/b") => ("a/", "/b")
743    let middle = paths.map(|path| path.get(prefix_len..path.len() - suffix_len).unwrap_or(""));
744
745    let mut collapsed = String::new();
746    collapsed.push_str(&paths.after[..prefix_len]);
747    collapsed.push('{');
748    collapsed.push_str(middle.before);
749    collapsed.push_str(" => ");
750    collapsed.push_str(middle.after);
751    collapsed.push('}');
752    collapsed.push_str(&paths.after[paths.after.len() - suffix_len..]);
753    collapsed
754}
755
756/// Tree that maps `RepoPath` to value of type `V`.
757#[derive(Clone, Default, Eq, PartialEq)]
758pub struct RepoPathTree<V> {
759    entries: HashMap<RepoPathComponentBuf, Self>,
760    value: V,
761}
762
763impl<V> RepoPathTree<V> {
764    /// The value associated with this path.
765    pub fn value(&self) -> &V {
766        &self.value
767    }
768
769    /// Mutable reference to the value associated with this path.
770    pub fn value_mut(&mut self) -> &mut V {
771        &mut self.value
772    }
773
774    /// Set the value associated with this path.
775    pub fn set_value(&mut self, value: V) {
776        self.value = value;
777    }
778
779    /// The immediate children of this node.
780    pub fn children(&self) -> impl Iterator<Item = (&RepoPathComponent, &Self)> {
781        self.entries
782            .iter()
783            .map(|(component, value)| (component.as_ref(), value))
784    }
785
786    /// Whether this node has any children.
787    pub fn has_children(&self) -> bool {
788        !self.entries.is_empty()
789    }
790
791    /// Add a path to the tree. Normally called on the root tree.
792    pub fn add(&mut self, path: &RepoPath) -> &mut Self
793    where
794        V: Default,
795    {
796        path.components().fold(self, |sub, name| {
797            // Avoid name.clone() if entry already exists.
798            if !sub.entries.contains_key(name) {
799                sub.entries.insert(name.to_owned(), Self::default());
800            }
801            sub.entries.get_mut(name).unwrap()
802        })
803    }
804
805    /// Get a reference to the node for the given `path`, if it exists in the
806    /// tree.
807    pub fn get(&self, path: &RepoPath) -> Option<&Self> {
808        path.components()
809            .try_fold(self, |sub, name| sub.entries.get(name))
810    }
811
812    /// Walks the tree from the root to the given `path`, yielding each sub tree
813    /// and remaining path.
814    pub fn walk_to<'a, 'b>(
815        &'a self,
816        path: &'b RepoPath,
817    ) -> impl Iterator<Item = (&'a Self, &'b RepoPath)> {
818        iter::successors(Some((self, path)), |(sub, path)| {
819            let mut components = path.components();
820            let name = components.next()?;
821            Some((sub.entries.get(name)?, components.as_path()))
822        })
823    }
824}
825
826impl<V: Debug> Debug for RepoPathTree<V> {
827    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
828        self.value.fmt(f)?;
829        f.write_str(" ")?;
830        f.debug_map()
831            .entries(
832                self.entries
833                    .iter()
834                    .sorted_unstable_by_key(|&(name, _)| name),
835            )
836            .finish()
837    }
838}
839
840#[cfg(test)]
841mod tests {
842    use std::panic;
843
844    use assert_matches::assert_matches;
845    use itertools::Itertools as _;
846
847    use super::*;
848    use crate::tests::new_temp_dir;
849
850    fn repo_path(value: &str) -> &RepoPath {
851        RepoPath::from_internal_string(value).unwrap()
852    }
853
854    fn repo_path_component(value: &str) -> &RepoPathComponent {
855        RepoPathComponent::new(value).unwrap()
856    }
857
858    #[test]
859    fn test_is_root() {
860        assert!(RepoPath::root().is_root());
861        assert!(repo_path("").is_root());
862        assert!(!repo_path("foo").is_root());
863    }
864
865    #[test]
866    fn test_from_internal_string() {
867        let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
868        assert_eq!(repo_path_buf(""), RepoPathBuf::root());
869        assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
870        assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
871        assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
872        assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
873
874        assert_eq!(repo_path(""), RepoPath::root());
875        assert!(panic::catch_unwind(|| repo_path("/")).is_err());
876        assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
877        assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
878        assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
879    }
880
881    #[test]
882    fn test_as_internal_file_string() {
883        assert_eq!(RepoPath::root().as_internal_file_string(), "");
884        assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
885        assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
886    }
887
888    #[test]
889    fn test_to_internal_dir_string() {
890        assert_eq!(RepoPath::root().to_internal_dir_string(), "");
891        assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
892        assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
893    }
894
895    #[test]
896    fn test_starts_with() {
897        assert!(repo_path("").starts_with(repo_path("")));
898        assert!(repo_path("x").starts_with(repo_path("")));
899        assert!(!repo_path("").starts_with(repo_path("x")));
900
901        assert!(repo_path("x").starts_with(repo_path("x")));
902        assert!(repo_path("x/y").starts_with(repo_path("x")));
903        assert!(!repo_path("xy").starts_with(repo_path("x")));
904        assert!(!repo_path("x/y").starts_with(repo_path("y")));
905
906        assert!(repo_path("x/y").starts_with(repo_path("x/y")));
907        assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
908        assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
909        assert!(!repo_path("x").starts_with(repo_path("x/y")));
910        assert!(!repo_path("xy").starts_with(repo_path("x/y")));
911    }
912
913    #[test]
914    fn test_strip_prefix() {
915        assert_eq!(
916            repo_path("").strip_prefix(repo_path("")),
917            Some(repo_path(""))
918        );
919        assert_eq!(
920            repo_path("x").strip_prefix(repo_path("")),
921            Some(repo_path("x"))
922        );
923        assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
924
925        assert_eq!(
926            repo_path("x").strip_prefix(repo_path("x")),
927            Some(repo_path(""))
928        );
929        assert_eq!(
930            repo_path("x/y").strip_prefix(repo_path("x")),
931            Some(repo_path("y"))
932        );
933        assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
934        assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
935
936        assert_eq!(
937            repo_path("x/y").strip_prefix(repo_path("x/y")),
938            Some(repo_path(""))
939        );
940        assert_eq!(
941            repo_path("x/y/z").strip_prefix(repo_path("x/y")),
942            Some(repo_path("z"))
943        );
944        assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
945        assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
946        assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
947    }
948
949    #[test]
950    fn test_order() {
951        assert!(RepoPath::root() < repo_path("dir"));
952        assert!(repo_path("dir") < repo_path("dirx"));
953        // '#' < '/', but ["dir", "sub"] < ["dir#"]
954        assert!(repo_path("dir") < repo_path("dir#"));
955        assert!(repo_path("dir") < repo_path("dir/sub"));
956        assert!(repo_path("dir/sub") < repo_path("dir#"));
957
958        assert!(repo_path("abc") < repo_path("dir/file"));
959        assert!(repo_path("dir") < repo_path("dir/file"));
960        assert!(repo_path("dis") > repo_path("dir/file"));
961        assert!(repo_path("xyz") > repo_path("dir/file"));
962        assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
963    }
964
965    #[test]
966    fn test_join() {
967        let root = RepoPath::root();
968        let dir = root.join(repo_path_component("dir"));
969        assert_eq!(dir.as_ref(), repo_path("dir"));
970        let subdir = dir.join(repo_path_component("subdir"));
971        assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
972        assert_eq!(
973            subdir.join(repo_path_component("file")).as_ref(),
974            repo_path("dir/subdir/file")
975        );
976    }
977
978    #[test]
979    fn test_extend() {
980        let mut path = RepoPathBuf::root();
981        path.extend(std::iter::empty::<RepoPathComponentBuf>());
982        assert_eq!(path.as_ref(), RepoPath::root());
983        path.extend([repo_path_component("dir")]);
984        assert_eq!(path.as_ref(), repo_path("dir"));
985        path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
986        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
987        path.extend(std::iter::empty::<RepoPathComponentBuf>());
988        assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
989    }
990
991    #[test]
992    fn test_parent() {
993        let root = RepoPath::root();
994        let dir_component = repo_path_component("dir");
995        let subdir_component = repo_path_component("subdir");
996
997        let dir = root.join(dir_component);
998        let subdir = dir.join(subdir_component);
999
1000        assert_eq!(root.parent(), None);
1001        assert_eq!(dir.parent(), Some(root));
1002        assert_eq!(subdir.parent(), Some(dir.as_ref()));
1003    }
1004
1005    #[test]
1006    fn test_split() {
1007        let root = RepoPath::root();
1008        let dir_component = repo_path_component("dir");
1009        let file_component = repo_path_component("file");
1010
1011        let dir = root.join(dir_component);
1012        let file = dir.join(file_component);
1013
1014        assert_eq!(root.split(), None);
1015        assert_eq!(dir.split(), Some((root, dir_component)));
1016        assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
1017    }
1018
1019    #[test]
1020    fn test_components() {
1021        assert!(RepoPath::root().components().next().is_none());
1022        assert_eq!(
1023            repo_path("dir").components().collect_vec(),
1024            vec![repo_path_component("dir")]
1025        );
1026        assert_eq!(
1027            repo_path("dir/subdir").components().collect_vec(),
1028            vec![repo_path_component("dir"), repo_path_component("subdir")]
1029        );
1030
1031        // Iterates from back
1032        assert!(RepoPath::root().components().next_back().is_none());
1033        assert_eq!(
1034            repo_path("dir").components().rev().collect_vec(),
1035            vec![repo_path_component("dir")]
1036        );
1037        assert_eq!(
1038            repo_path("dir/subdir").components().rev().collect_vec(),
1039            vec![repo_path_component("subdir"), repo_path_component("dir")]
1040        );
1041    }
1042
1043    #[test]
1044    fn test_ancestors() {
1045        assert_eq!(
1046            RepoPath::root().ancestors().collect_vec(),
1047            vec![RepoPath::root()]
1048        );
1049        assert_eq!(
1050            repo_path("dir").ancestors().collect_vec(),
1051            vec![repo_path("dir"), RepoPath::root()]
1052        );
1053        assert_eq!(
1054            repo_path("dir/subdir").ancestors().collect_vec(),
1055            vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
1056        );
1057    }
1058
1059    #[test]
1060    fn test_to_fs_path() {
1061        assert_eq!(
1062            repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
1063            Path::new("base/dir")
1064        );
1065        assert_eq!(
1066            repo_path("").to_fs_path(Path::new("")).unwrap(),
1067            Path::new(".")
1068        );
1069        assert_eq!(
1070            repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
1071            Path::new("base/dir/file")
1072        );
1073        assert_eq!(
1074            repo_path("some/deep/dir/file")
1075                .to_fs_path(Path::new("base/dir"))
1076                .unwrap(),
1077            Path::new("base/dir/some/deep/dir/file")
1078        );
1079        assert_eq!(
1080            repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
1081            Path::new("dir/file")
1082        );
1083
1084        // Current/parent dir component
1085        assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
1086        assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
1087        assert!(
1088            repo_path("dir/../file")
1089                .to_fs_path(Path::new("base"))
1090                .is_err()
1091        );
1092        assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
1093        assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
1094        assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
1095        assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
1096
1097        // Empty component (which is invalid as a repo path)
1098        assert!(
1099            RepoPath::from_internal_string_unchecked("/")
1100                .to_fs_path(Path::new("base"))
1101                .is_err()
1102        );
1103        assert_eq!(
1104            // Iterator omits empty component after "/", which is fine so long
1105            // as the returned path doesn't escape.
1106            RepoPath::from_internal_string_unchecked("a/")
1107                .to_fs_path(Path::new("base"))
1108                .unwrap(),
1109            Path::new("base/a")
1110        );
1111        assert!(
1112            RepoPath::from_internal_string_unchecked("/b")
1113                .to_fs_path(Path::new("base"))
1114                .is_err()
1115        );
1116        assert!(
1117            RepoPath::from_internal_string_unchecked("a//b")
1118                .to_fs_path(Path::new("base"))
1119                .is_err()
1120        );
1121
1122        // Component containing slash (simulating Windows path separator)
1123        assert!(
1124            RepoPathComponent::new_unchecked("wind/ows")
1125                .to_fs_name()
1126                .is_err()
1127        );
1128        assert!(
1129            RepoPathComponent::new_unchecked("./file")
1130                .to_fs_name()
1131                .is_err()
1132        );
1133        assert!(
1134            RepoPathComponent::new_unchecked("file/.")
1135                .to_fs_name()
1136                .is_err()
1137        );
1138        assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
1139
1140        // Windows path separator and drive letter
1141        if cfg!(windows) {
1142            assert!(
1143                repo_path(r#"wind\ows"#)
1144                    .to_fs_path(Path::new("base"))
1145                    .is_err()
1146            );
1147            assert!(
1148                repo_path(r#".\file"#)
1149                    .to_fs_path(Path::new("base"))
1150                    .is_err()
1151            );
1152            assert!(
1153                repo_path(r#"file\."#)
1154                    .to_fs_path(Path::new("base"))
1155                    .is_err()
1156            );
1157            assert!(
1158                repo_path(r#"c:/foo"#)
1159                    .to_fs_path(Path::new("base"))
1160                    .is_err()
1161            );
1162        }
1163    }
1164
1165    #[test]
1166    fn test_to_fs_path_unchecked() {
1167        assert_eq!(
1168            repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1169            Path::new("base/dir")
1170        );
1171        assert_eq!(
1172            repo_path("").to_fs_path_unchecked(Path::new("")),
1173            Path::new(".")
1174        );
1175        assert_eq!(
1176            repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1177            Path::new("base/dir/file")
1178        );
1179        assert_eq!(
1180            repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1181            Path::new("base/dir/some/deep/dir/file")
1182        );
1183        assert_eq!(
1184            repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1185            Path::new("dir/file")
1186        );
1187    }
1188
1189    #[test]
1190    fn parse_fs_path_wc_in_cwd() {
1191        let temp_dir = new_temp_dir();
1192        let cwd_path = temp_dir.path().join("repo");
1193        let wc_path = &cwd_path;
1194
1195        assert_eq!(
1196            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1197            Ok(RepoPath::root())
1198        );
1199        assert_eq!(
1200            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1201            Ok(RepoPath::root())
1202        );
1203        assert_eq!(
1204            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1205            Ok(repo_path("file"))
1206        );
1207        // Both slash and the platform's separator are allowed
1208        assert_eq!(
1209            RepoPathBuf::parse_fs_path(
1210                &cwd_path,
1211                wc_path,
1212                format!("dir{}file", std::path::MAIN_SEPARATOR)
1213            )
1214            .as_deref(),
1215            Ok(repo_path("dir/file"))
1216        );
1217        assert_eq!(
1218            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1219            Ok(repo_path("dir/file"))
1220        );
1221        assert_matches!(
1222            RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1223            Err(FsPathParseError {
1224                source: RelativePathParseError::InvalidComponent { .. },
1225                ..
1226            })
1227        );
1228        assert_eq!(
1229            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1230            Ok(RepoPath::root())
1231        );
1232        assert_eq!(
1233            RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1234            Ok(repo_path("file"))
1235        );
1236        // Input may be absolute path with ".."
1237        assert_eq!(
1238            RepoPathBuf::parse_fs_path(
1239                &cwd_path,
1240                &cwd_path,
1241                cwd_path.join("../repo").to_str().unwrap()
1242            )
1243            .as_deref(),
1244            Ok(RepoPath::root())
1245        );
1246    }
1247
1248    #[test]
1249    fn parse_fs_path_wc_in_cwd_parent() {
1250        let temp_dir = new_temp_dir();
1251        let cwd_path = temp_dir.path().join("dir");
1252        let wc_path = cwd_path.parent().unwrap().to_path_buf();
1253
1254        assert_eq!(
1255            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1256            Ok(repo_path("dir"))
1257        );
1258        assert_eq!(
1259            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1260            Ok(repo_path("dir"))
1261        );
1262        assert_eq!(
1263            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1264            Ok(repo_path("dir/file"))
1265        );
1266        assert_eq!(
1267            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1268            Ok(repo_path("dir/subdir/file"))
1269        );
1270        assert_eq!(
1271            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1272            Ok(RepoPath::root())
1273        );
1274        assert_matches!(
1275            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1276            Err(FsPathParseError {
1277                source: RelativePathParseError::InvalidComponent { .. },
1278                ..
1279            })
1280        );
1281        assert_eq!(
1282            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1283            Ok(repo_path("other-dir/file"))
1284        );
1285    }
1286
1287    #[test]
1288    fn parse_fs_path_wc_in_cwd_child() {
1289        let temp_dir = new_temp_dir();
1290        let cwd_path = temp_dir.path().join("cwd");
1291        let wc_path = cwd_path.join("repo");
1292
1293        assert_matches!(
1294            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1295            Err(FsPathParseError {
1296                source: RelativePathParseError::InvalidComponent { .. },
1297                ..
1298            })
1299        );
1300        assert_matches!(
1301            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1302            Err(FsPathParseError {
1303                source: RelativePathParseError::InvalidComponent { .. },
1304                ..
1305            })
1306        );
1307        assert_eq!(
1308            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1309            Ok(RepoPath::root())
1310        );
1311        assert_eq!(
1312            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1313            Ok(repo_path("file"))
1314        );
1315        assert_eq!(
1316            RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1317            Ok(repo_path("dir/file"))
1318        );
1319    }
1320
1321    #[test]
1322    fn test_format_copied_path() {
1323        let ui = RepoPathUiConverter::Fs {
1324            cwd: PathBuf::from("."),
1325            base: PathBuf::from("."),
1326        };
1327
1328        let format = |before, after| {
1329            ui.format_copied_path(Diff::new(repo_path(before), repo_path(after)))
1330                .replace('\\', "/")
1331        };
1332
1333        assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1334        assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1335        assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1336        assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1337        assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1338        assert_eq!(
1339            format("one/two", "one/two/three/one/two"),
1340            "one/{ => two/three/one}/two"
1341        );
1342
1343        assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1344        assert_eq!(
1345            format("one/two/three", "one/four/three"),
1346            "one/{two => four}/three"
1347        );
1348        assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1349        assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1350        assert_eq!(format("two", "four"), "{two => four}");
1351        assert_eq!(format("file1", "file2"), "{file1 => file2}");
1352        assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1353        assert_eq!(
1354            format("x/something/something/2to1.txt", "x/something/2to1.txt"),
1355            "x/something/{something => }/2to1.txt"
1356        );
1357        assert_eq!(
1358            format("x/something/1to2.txt", "x/something/something/1to2.txt"),
1359            "x/something/{ => something}/1to2.txt"
1360        );
1361    }
1362
1363    #[test]
1364    fn test_split_common_prefix() {
1365        assert_eq!(
1366            repo_path("foo/bar").split_common_prefix(repo_path("foo/bar/baz")),
1367            (repo_path("foo/bar"), repo_path(""))
1368        );
1369
1370        assert_eq!(
1371            repo_path("foo/bar/baz").split_common_prefix(repo_path("foo/bar")),
1372            (repo_path("foo/bar"), repo_path("baz"))
1373        );
1374
1375        assert_eq!(
1376            repo_path("foo/bar/bing").split_common_prefix(repo_path("foo/bar/baz")),
1377            (repo_path("foo/bar"), repo_path("bing"))
1378        );
1379
1380        assert_eq!(
1381            repo_path("no/common/prefix").split_common_prefix(repo_path("foo/bar/baz")),
1382            (RepoPath::root(), repo_path("no/common/prefix"))
1383        );
1384
1385        assert_eq!(
1386            repo_path("same/path").split_common_prefix(repo_path("same/path")),
1387            (repo_path("same/path"), RepoPath::root())
1388        );
1389
1390        assert_eq!(
1391            RepoPath::root().split_common_prefix(repo_path("foo")),
1392            (RepoPath::root(), RepoPath::root())
1393        );
1394
1395        assert_eq!(
1396            RepoPath::root().split_common_prefix(RepoPath::root()),
1397            (RepoPath::root(), RepoPath::root())
1398        );
1399
1400        assert_eq!(
1401            repo_path("foo/bar").split_common_prefix(RepoPath::root()),
1402            (RepoPath::root(), repo_path("foo/bar"))
1403        );
1404    }
1405}