1#![allow(missing_docs)]
16
17use std::borrow::Borrow;
18use std::cmp::Ordering;
19use std::fmt;
20use std::fmt::Debug;
21use std::fmt::Formatter;
22use std::iter::FusedIterator;
23use std::ops::Deref;
24use std::path::Component;
25use std::path::Path;
26use std::path::PathBuf;
27
28use itertools::Itertools as _;
29use ref_cast::ref_cast_custom;
30use ref_cast::RefCastCustom;
31use thiserror::Error;
32
33use crate::content_hash::ContentHash;
34use crate::file_util;
35
36#[derive(ContentHash, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38pub struct RepoPathComponentBuf {
39 value: String,
42}
43
44impl RepoPathComponentBuf {
45 pub fn new(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
50 let value: String = value.into();
51 if is_valid_repo_path_component_str(&value) {
52 Ok(Self { value })
53 } else {
54 Err(InvalidNewRepoPathError { value })
55 }
56 }
57}
58
59#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, RefCastCustom)]
61#[repr(transparent)]
62pub struct RepoPathComponent {
63 value: str,
64}
65
66impl RepoPathComponent {
67 pub fn new(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
72 if is_valid_repo_path_component_str(value) {
73 Ok(Self::new_unchecked(value))
74 } else {
75 Err(InvalidNewRepoPathError {
76 value: value.to_string(),
77 })
78 }
79 }
80
81 #[ref_cast_custom]
82 const fn new_unchecked(value: &str) -> &Self;
83
84 pub fn as_internal_str(&self) -> &str {
86 &self.value
87 }
88
89 pub fn to_fs_name(&self) -> Result<&str, InvalidRepoPathComponentError> {
92 let mut components = Path::new(&self.value).components().fuse();
93 match (components.next(), components.next()) {
94 (Some(Component::Normal(name)), None) if name == &self.value => Ok(&self.value),
97 _ => Err(InvalidRepoPathComponentError {
99 component: self.value.into(),
100 }),
101 }
102 }
103}
104
105impl Debug for RepoPathComponent {
106 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
107 write!(f, "{:?}", &self.value)
108 }
109}
110
111impl Debug for RepoPathComponentBuf {
112 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
113 <RepoPathComponent as Debug>::fmt(self, f)
114 }
115}
116
117impl AsRef<RepoPathComponent> for RepoPathComponent {
118 fn as_ref(&self) -> &RepoPathComponent {
119 self
120 }
121}
122
123impl AsRef<RepoPathComponent> for RepoPathComponentBuf {
124 fn as_ref(&self) -> &RepoPathComponent {
125 self
126 }
127}
128
129impl Borrow<RepoPathComponent> for RepoPathComponentBuf {
130 fn borrow(&self) -> &RepoPathComponent {
131 self
132 }
133}
134
135impl Deref for RepoPathComponentBuf {
136 type Target = RepoPathComponent;
137
138 fn deref(&self) -> &Self::Target {
139 RepoPathComponent::new_unchecked(&self.value)
140 }
141}
142
143impl ToOwned for RepoPathComponent {
144 type Owned = RepoPathComponentBuf;
145
146 fn to_owned(&self) -> Self::Owned {
147 let value = self.value.to_owned();
148 RepoPathComponentBuf { value }
149 }
150
151 fn clone_into(&self, target: &mut Self::Owned) {
152 self.value.clone_into(&mut target.value);
153 }
154}
155
156#[derive(Clone, Debug)]
158pub struct RepoPathComponentsIter<'a> {
159 value: &'a str,
160}
161
162impl<'a> RepoPathComponentsIter<'a> {
163 pub fn as_path(&self) -> &'a RepoPath {
165 RepoPath::from_internal_string_unchecked(self.value)
166 }
167}
168
169impl<'a> Iterator for RepoPathComponentsIter<'a> {
170 type Item = &'a RepoPathComponent;
171
172 fn next(&mut self) -> Option<Self::Item> {
173 if self.value.is_empty() {
174 return None;
175 }
176 let (name, remainder) = self
177 .value
178 .split_once('/')
179 .unwrap_or_else(|| (self.value, &self.value[self.value.len()..]));
180 self.value = remainder;
181 Some(RepoPathComponent::new_unchecked(name))
182 }
183}
184
185impl DoubleEndedIterator for RepoPathComponentsIter<'_> {
186 fn next_back(&mut self) -> Option<Self::Item> {
187 if self.value.is_empty() {
188 return None;
189 }
190 let (remainder, name) = self
191 .value
192 .rsplit_once('/')
193 .unwrap_or_else(|| (&self.value[..0], self.value));
194 self.value = remainder;
195 Some(RepoPathComponent::new_unchecked(name))
196 }
197}
198
199impl FusedIterator for RepoPathComponentsIter<'_> {}
200
201#[derive(ContentHash, Clone, Eq, Hash, PartialEq)]
203pub struct RepoPathBuf {
204 value: String,
207}
208
209#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom)]
211#[repr(transparent)]
212pub struct RepoPath {
213 value: str,
214}
215
216impl Debug for RepoPath {
217 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
218 write!(f, "{:?}", &self.value)
219 }
220}
221
222impl Debug for RepoPathBuf {
223 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
224 <RepoPath as Debug>::fmt(self, f)
225 }
226}
227
228#[derive(Clone, Debug, Eq, Error, PartialEq)]
232#[error(r#"Invalid repo path input "{value}""#)]
233pub struct InvalidNewRepoPathError {
234 value: String,
235}
236
237impl RepoPathBuf {
238 pub const fn root() -> Self {
240 RepoPathBuf {
241 value: String::new(),
242 }
243 }
244
245 pub fn from_internal_string(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
247 let value: String = value.into();
248 if is_valid_repo_path_str(&value) {
249 Ok(RepoPathBuf { value })
250 } else {
251 Err(InvalidNewRepoPathError { value })
252 }
253 }
254
255 pub fn from_relative_path(
259 relative_path: impl AsRef<Path>,
260 ) -> Result<Self, RelativePathParseError> {
261 let relative_path = relative_path.as_ref();
262 if relative_path == Path::new(".") {
263 return Ok(Self::root());
264 }
265
266 let mut components = relative_path
267 .components()
268 .map(|c| match c {
269 Component::Normal(name) => {
270 name.to_str()
271 .ok_or_else(|| RelativePathParseError::InvalidUtf8 {
272 path: relative_path.into(),
273 })
274 }
275 _ => Err(RelativePathParseError::InvalidComponent {
276 component: c.as_os_str().to_string_lossy().into(),
277 path: relative_path.into(),
278 }),
279 })
280 .fuse();
281 let mut value = String::with_capacity(relative_path.as_os_str().len());
282 if let Some(name) = components.next() {
283 value.push_str(name?);
284 }
285 for name in components {
286 value.push('/');
287 value.push_str(name?);
288 }
289 Ok(RepoPathBuf { value })
290 }
291
292 pub fn parse_fs_path(
298 cwd: &Path,
299 base: &Path,
300 input: impl AsRef<Path>,
301 ) -> Result<Self, FsPathParseError> {
302 let input = input.as_ref();
303 let abs_input_path = file_util::normalize_path(&cwd.join(input));
304 let repo_relative_path = file_util::relative_path(base, &abs_input_path);
305 Self::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError {
306 base: file_util::relative_path(cwd, base).into(),
307 input: input.into(),
308 source,
309 })
310 }
311
312 pub fn into_internal_string(self) -> String {
314 self.value
315 }
316}
317
318impl RepoPath {
319 pub const fn root() -> &'static Self {
321 Self::from_internal_string_unchecked("")
322 }
323
324 pub fn from_internal_string(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
329 if is_valid_repo_path_str(value) {
330 Ok(Self::from_internal_string_unchecked(value))
331 } else {
332 Err(InvalidNewRepoPathError {
333 value: value.to_owned(),
334 })
335 }
336 }
337
338 #[ref_cast_custom]
339 const fn from_internal_string_unchecked(value: &str) -> &Self;
340
341 pub fn to_internal_dir_string(&self) -> String {
346 if self.value.is_empty() {
347 String::new()
348 } else {
349 [&self.value, "/"].concat()
350 }
351 }
352
353 pub fn as_internal_file_string(&self) -> &str {
356 &self.value
357 }
358
359 pub fn to_fs_path(&self, base: &Path) -> Result<PathBuf, InvalidRepoPathError> {
364 let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
365 result.push(base);
366 for c in self.components() {
367 result.push(c.to_fs_name().map_err(|err| err.with_path(self))?);
368 }
369 if result.as_os_str().is_empty() {
370 result.push(".");
371 }
372 Ok(result)
373 }
374
375 pub fn to_fs_path_unchecked(&self, base: &Path) -> PathBuf {
381 let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
382 result.push(base);
383 result.extend(self.components().map(RepoPathComponent::as_internal_str));
384 if result.as_os_str().is_empty() {
385 result.push(".");
386 }
387 result
388 }
389
390 pub fn is_root(&self) -> bool {
391 self.value.is_empty()
392 }
393
394 pub fn starts_with(&self, base: &RepoPath) -> bool {
396 self.strip_prefix(base).is_some()
397 }
398
399 pub fn strip_prefix(&self, base: &RepoPath) -> Option<&RepoPath> {
401 if base.value.is_empty() {
402 Some(self)
403 } else {
404 let tail = self.value.strip_prefix(&base.value)?;
405 if tail.is_empty() {
406 Some(RepoPath::from_internal_string_unchecked(tail))
407 } else {
408 tail.strip_prefix('/')
409 .map(RepoPath::from_internal_string_unchecked)
410 }
411 }
412 }
413
414 pub fn parent(&self) -> Option<&RepoPath> {
416 self.split().map(|(parent, _)| parent)
417 }
418
419 pub fn split(&self) -> Option<(&RepoPath, &RepoPathComponent)> {
421 let mut components = self.components();
422 let basename = components.next_back()?;
423 Some((components.as_path(), basename))
424 }
425
426 pub fn components(&self) -> RepoPathComponentsIter<'_> {
427 RepoPathComponentsIter { value: &self.value }
428 }
429
430 pub fn ancestors(&self) -> impl Iterator<Item = &RepoPath> {
431 std::iter::successors(Some(self), |path| path.parent())
432 }
433
434 pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf {
435 let value = if self.value.is_empty() {
436 entry.as_internal_str().to_owned()
437 } else {
438 [&self.value, "/", entry.as_internal_str()].concat()
439 };
440 RepoPathBuf { value }
441 }
442}
443
444impl AsRef<RepoPath> for RepoPath {
445 fn as_ref(&self) -> &RepoPath {
446 self
447 }
448}
449
450impl AsRef<RepoPath> for RepoPathBuf {
451 fn as_ref(&self) -> &RepoPath {
452 self
453 }
454}
455
456impl Borrow<RepoPath> for RepoPathBuf {
457 fn borrow(&self) -> &RepoPath {
458 self
459 }
460}
461
462impl Deref for RepoPathBuf {
463 type Target = RepoPath;
464
465 fn deref(&self) -> &Self::Target {
466 RepoPath::from_internal_string_unchecked(&self.value)
467 }
468}
469
470impl ToOwned for RepoPath {
471 type Owned = RepoPathBuf;
472
473 fn to_owned(&self) -> Self::Owned {
474 let value = self.value.to_owned();
475 RepoPathBuf { value }
476 }
477
478 fn clone_into(&self, target: &mut Self::Owned) {
479 self.value.clone_into(&mut target.value);
480 }
481}
482
483impl Ord for RepoPath {
484 fn cmp(&self, other: &Self) -> Ordering {
485 debug_assert!(is_valid_repo_path_str(&self.value));
488 self.components().cmp(other.components())
489 }
490}
491
492impl Ord for RepoPathBuf {
493 fn cmp(&self, other: &Self) -> Ordering {
494 <RepoPath as Ord>::cmp(self, other)
495 }
496}
497
498impl PartialOrd for RepoPath {
499 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
500 Some(self.cmp(other))
501 }
502}
503
504impl PartialOrd for RepoPathBuf {
505 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
506 Some(self.cmp(other))
507 }
508}
509
510impl<P: AsRef<RepoPathComponent>> Extend<P> for RepoPathBuf {
511 fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
512 for component in iter {
513 if !self.value.is_empty() {
514 self.value.push('/');
515 }
516 self.value.push_str(component.as_ref().as_internal_str());
517 }
518 }
519}
520
521#[derive(Clone, Debug, Eq, Error, PartialEq)]
523#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())]
524pub struct InvalidRepoPathError {
525 pub path: RepoPathBuf,
527 pub source: InvalidRepoPathComponentError,
529}
530
531#[derive(Clone, Debug, Eq, Error, PartialEq)]
533#[error(r#"Invalid path component "{component}""#)]
534pub struct InvalidRepoPathComponentError {
535 pub component: Box<str>,
536}
537
538impl InvalidRepoPathComponentError {
539 pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError {
541 InvalidRepoPathError {
542 path: path.to_owned(),
543 source: self,
544 }
545 }
546}
547
548#[derive(Clone, Debug, Eq, Error, PartialEq)]
549pub enum RelativePathParseError {
550 #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
551 InvalidComponent {
552 component: Box<str>,
553 path: Box<Path>,
554 },
555 #[error(r#"Not valid UTF-8 path "{path}""#)]
556 InvalidUtf8 { path: Box<Path> },
557}
558
559#[derive(Clone, Debug, Eq, Error, PartialEq)]
560#[error(r#"Path "{input}" is not in the repo "{base}""#)]
561pub struct FsPathParseError {
562 pub base: Box<Path>,
564 pub input: Box<Path>,
566 pub source: RelativePathParseError,
568}
569
570fn is_valid_repo_path_component_str(value: &str) -> bool {
571 !value.is_empty() && !value.contains('/')
572}
573
574fn is_valid_repo_path_str(value: &str) -> bool {
575 !value.starts_with('/') && !value.ends_with('/') && !value.contains("//")
576}
577
578#[derive(Debug, Error)]
580pub enum UiPathParseError {
581 #[error(transparent)]
582 Fs(FsPathParseError),
583}
584
585#[derive(Debug, Clone)]
588pub enum RepoPathUiConverter {
589 Fs { cwd: PathBuf, base: PathBuf },
595 }
598
599impl RepoPathUiConverter {
600 pub fn format_file_path(&self, file: &RepoPath) -> String {
602 match self {
603 RepoPathUiConverter::Fs { cwd, base } => {
604 file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
605 .to_str()
606 .unwrap()
607 .to_owned()
608 }
609 }
610 }
611
612 pub fn format_copied_path(&self, source: &RepoPath, target: &RepoPath) -> String {
618 if source == target {
619 return self.format_file_path(source);
620 }
621 let mut formatted = String::new();
622 match self {
623 RepoPathUiConverter::Fs { cwd, base } => {
624 let source_path = file_util::relative_path(cwd, &source.to_fs_path_unchecked(base));
625 let target_path = file_util::relative_path(cwd, &target.to_fs_path_unchecked(base));
626
627 let source_components = source_path.components().collect_vec();
628 let target_components = target_path.components().collect_vec();
629
630 let prefix_count = source_components
631 .iter()
632 .zip(target_components.iter())
633 .take_while(|(source_component, target_component)| {
634 source_component == target_component
635 })
636 .count()
637 .min(source_components.len().saturating_sub(1))
638 .min(target_components.len().saturating_sub(1));
639
640 let suffix_count = source_components
641 .iter()
642 .rev()
643 .zip(target_components.iter().rev())
644 .take_while(|(source_component, target_component)| {
645 source_component == target_component
646 })
647 .count()
648 .min(source_components.len().saturating_sub(1))
649 .min(target_components.len().saturating_sub(1));
650
651 fn format_components(c: &[std::path::Component]) -> String {
652 c.iter().collect::<PathBuf>().to_str().unwrap().to_owned()
653 }
654
655 if prefix_count > 0 {
656 formatted.push_str(&format_components(&source_components[0..prefix_count]));
657 formatted.push_str(std::path::MAIN_SEPARATOR_STR);
658 }
659 formatted.push('{');
660 formatted.push_str(&format_components(
661 &source_components
662 [prefix_count..(source_components.len() - suffix_count).max(prefix_count)],
663 ));
664 formatted.push_str(" => ");
665 formatted.push_str(&format_components(
666 &target_components
667 [prefix_count..(target_components.len() - suffix_count).max(prefix_count)],
668 ));
669 formatted.push('}');
670 if suffix_count > 0 {
671 formatted.push_str(std::path::MAIN_SEPARATOR_STR);
672 formatted.push_str(&format_components(
673 &source_components[source_components.len() - suffix_count..],
674 ));
675 }
676 }
677 }
678 formatted
679 }
680
681 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
686 match self {
687 RepoPathUiConverter::Fs { cwd, base } => {
688 RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
689 }
690 }
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use std::panic;
697
698 use assert_matches::assert_matches;
699 use itertools::Itertools as _;
700
701 use super::*;
702 use crate::tests::new_temp_dir;
703
704 fn repo_path(value: &str) -> &RepoPath {
705 RepoPath::from_internal_string(value).unwrap()
706 }
707
708 fn repo_path_component(value: &str) -> &RepoPathComponent {
709 RepoPathComponent::new(value).unwrap()
710 }
711
712 #[test]
713 fn test_is_root() {
714 assert!(RepoPath::root().is_root());
715 assert!(repo_path("").is_root());
716 assert!(!repo_path("foo").is_root());
717 }
718
719 #[test]
720 fn test_from_internal_string() {
721 let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
722 assert_eq!(repo_path_buf(""), RepoPathBuf::root());
723 assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
724 assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
725 assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
726 assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
727
728 assert_eq!(repo_path(""), RepoPath::root());
729 assert!(panic::catch_unwind(|| repo_path("/")).is_err());
730 assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
731 assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
732 assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
733 }
734
735 #[test]
736 fn test_as_internal_file_string() {
737 assert_eq!(RepoPath::root().as_internal_file_string(), "");
738 assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
739 assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
740 }
741
742 #[test]
743 fn test_to_internal_dir_string() {
744 assert_eq!(RepoPath::root().to_internal_dir_string(), "");
745 assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
746 assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
747 }
748
749 #[test]
750 fn test_starts_with() {
751 assert!(repo_path("").starts_with(repo_path("")));
752 assert!(repo_path("x").starts_with(repo_path("")));
753 assert!(!repo_path("").starts_with(repo_path("x")));
754
755 assert!(repo_path("x").starts_with(repo_path("x")));
756 assert!(repo_path("x/y").starts_with(repo_path("x")));
757 assert!(!repo_path("xy").starts_with(repo_path("x")));
758 assert!(!repo_path("x/y").starts_with(repo_path("y")));
759
760 assert!(repo_path("x/y").starts_with(repo_path("x/y")));
761 assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
762 assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
763 assert!(!repo_path("x").starts_with(repo_path("x/y")));
764 assert!(!repo_path("xy").starts_with(repo_path("x/y")));
765 }
766
767 #[test]
768 fn test_strip_prefix() {
769 assert_eq!(
770 repo_path("").strip_prefix(repo_path("")),
771 Some(repo_path(""))
772 );
773 assert_eq!(
774 repo_path("x").strip_prefix(repo_path("")),
775 Some(repo_path("x"))
776 );
777 assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
778
779 assert_eq!(
780 repo_path("x").strip_prefix(repo_path("x")),
781 Some(repo_path(""))
782 );
783 assert_eq!(
784 repo_path("x/y").strip_prefix(repo_path("x")),
785 Some(repo_path("y"))
786 );
787 assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
788 assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
789
790 assert_eq!(
791 repo_path("x/y").strip_prefix(repo_path("x/y")),
792 Some(repo_path(""))
793 );
794 assert_eq!(
795 repo_path("x/y/z").strip_prefix(repo_path("x/y")),
796 Some(repo_path("z"))
797 );
798 assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
799 assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
800 assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
801 }
802
803 #[test]
804 fn test_order() {
805 assert!(RepoPath::root() < repo_path("dir"));
806 assert!(repo_path("dir") < repo_path("dirx"));
807 assert!(repo_path("dir") < repo_path("dir#"));
809 assert!(repo_path("dir") < repo_path("dir/sub"));
810 assert!(repo_path("dir/sub") < repo_path("dir#"));
811
812 assert!(repo_path("abc") < repo_path("dir/file"));
813 assert!(repo_path("dir") < repo_path("dir/file"));
814 assert!(repo_path("dis") > repo_path("dir/file"));
815 assert!(repo_path("xyz") > repo_path("dir/file"));
816 assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
817 }
818
819 #[test]
820 fn test_join() {
821 let root = RepoPath::root();
822 let dir = root.join(repo_path_component("dir"));
823 assert_eq!(dir.as_ref(), repo_path("dir"));
824 let subdir = dir.join(repo_path_component("subdir"));
825 assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
826 assert_eq!(
827 subdir.join(repo_path_component("file")).as_ref(),
828 repo_path("dir/subdir/file")
829 );
830 }
831
832 #[test]
833 fn test_extend() {
834 let mut path = RepoPathBuf::root();
835 path.extend(std::iter::empty::<RepoPathComponentBuf>());
836 assert_eq!(path.as_ref(), RepoPath::root());
837 path.extend([repo_path_component("dir")]);
838 assert_eq!(path.as_ref(), repo_path("dir"));
839 path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
840 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
841 path.extend(std::iter::empty::<RepoPathComponentBuf>());
842 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
843 }
844
845 #[test]
846 fn test_parent() {
847 let root = RepoPath::root();
848 let dir_component = repo_path_component("dir");
849 let subdir_component = repo_path_component("subdir");
850
851 let dir = root.join(dir_component);
852 let subdir = dir.join(subdir_component);
853
854 assert_eq!(root.parent(), None);
855 assert_eq!(dir.parent(), Some(root));
856 assert_eq!(subdir.parent(), Some(dir.as_ref()));
857 }
858
859 #[test]
860 fn test_split() {
861 let root = RepoPath::root();
862 let dir_component = repo_path_component("dir");
863 let file_component = repo_path_component("file");
864
865 let dir = root.join(dir_component);
866 let file = dir.join(file_component);
867
868 assert_eq!(root.split(), None);
869 assert_eq!(dir.split(), Some((root, dir_component)));
870 assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
871 }
872
873 #[test]
874 fn test_components() {
875 assert!(RepoPath::root().components().next().is_none());
876 assert_eq!(
877 repo_path("dir").components().collect_vec(),
878 vec![repo_path_component("dir")]
879 );
880 assert_eq!(
881 repo_path("dir/subdir").components().collect_vec(),
882 vec![repo_path_component("dir"), repo_path_component("subdir")]
883 );
884
885 assert!(RepoPath::root().components().next_back().is_none());
887 assert_eq!(
888 repo_path("dir").components().rev().collect_vec(),
889 vec![repo_path_component("dir")]
890 );
891 assert_eq!(
892 repo_path("dir/subdir").components().rev().collect_vec(),
893 vec![repo_path_component("subdir"), repo_path_component("dir")]
894 );
895 }
896
897 #[test]
898 fn test_ancestors() {
899 assert_eq!(
900 RepoPath::root().ancestors().collect_vec(),
901 vec![RepoPath::root()]
902 );
903 assert_eq!(
904 repo_path("dir").ancestors().collect_vec(),
905 vec![repo_path("dir"), RepoPath::root()]
906 );
907 assert_eq!(
908 repo_path("dir/subdir").ancestors().collect_vec(),
909 vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
910 );
911 }
912
913 #[test]
914 fn test_to_fs_path() {
915 assert_eq!(
916 repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
917 Path::new("base/dir")
918 );
919 assert_eq!(
920 repo_path("").to_fs_path(Path::new("")).unwrap(),
921 Path::new(".")
922 );
923 assert_eq!(
924 repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
925 Path::new("base/dir/file")
926 );
927 assert_eq!(
928 repo_path("some/deep/dir/file")
929 .to_fs_path(Path::new("base/dir"))
930 .unwrap(),
931 Path::new("base/dir/some/deep/dir/file")
932 );
933 assert_eq!(
934 repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
935 Path::new("dir/file")
936 );
937
938 assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
940 assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
941 assert!(repo_path("dir/../file")
942 .to_fs_path(Path::new("base"))
943 .is_err());
944 assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
945 assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
946 assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
947 assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
948
949 assert!(RepoPath::from_internal_string_unchecked("/")
951 .to_fs_path(Path::new("base"))
952 .is_err());
953 assert_eq!(
954 RepoPath::from_internal_string_unchecked("a/")
957 .to_fs_path(Path::new("base"))
958 .unwrap(),
959 Path::new("base/a")
960 );
961 assert!(RepoPath::from_internal_string_unchecked("/b")
962 .to_fs_path(Path::new("base"))
963 .is_err());
964 assert!(RepoPath::from_internal_string_unchecked("a//b")
965 .to_fs_path(Path::new("base"))
966 .is_err());
967
968 assert!(RepoPathComponent::new_unchecked("wind/ows")
970 .to_fs_name()
971 .is_err());
972 assert!(RepoPathComponent::new_unchecked("./file")
973 .to_fs_name()
974 .is_err());
975 assert!(RepoPathComponent::new_unchecked("file/.")
976 .to_fs_name()
977 .is_err());
978 assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
979
980 if cfg!(windows) {
982 assert!(repo_path(r#"wind\ows"#)
983 .to_fs_path(Path::new("base"))
984 .is_err());
985 assert!(repo_path(r#".\file"#)
986 .to_fs_path(Path::new("base"))
987 .is_err());
988 assert!(repo_path(r#"file\."#)
989 .to_fs_path(Path::new("base"))
990 .is_err());
991 assert!(repo_path(r#"c:/foo"#)
992 .to_fs_path(Path::new("base"))
993 .is_err());
994 }
995 }
996
997 #[test]
998 fn test_to_fs_path_unchecked() {
999 assert_eq!(
1000 repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1001 Path::new("base/dir")
1002 );
1003 assert_eq!(
1004 repo_path("").to_fs_path_unchecked(Path::new("")),
1005 Path::new(".")
1006 );
1007 assert_eq!(
1008 repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1009 Path::new("base/dir/file")
1010 );
1011 assert_eq!(
1012 repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1013 Path::new("base/dir/some/deep/dir/file")
1014 );
1015 assert_eq!(
1016 repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1017 Path::new("dir/file")
1018 );
1019 }
1020
1021 #[test]
1022 fn parse_fs_path_wc_in_cwd() {
1023 let temp_dir = new_temp_dir();
1024 let cwd_path = temp_dir.path().join("repo");
1025 let wc_path = &cwd_path;
1026
1027 assert_eq!(
1028 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1029 Ok(RepoPath::root())
1030 );
1031 assert_eq!(
1032 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1033 Ok(RepoPath::root())
1034 );
1035 assert_eq!(
1036 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1037 Ok(repo_path("file"))
1038 );
1039 assert_eq!(
1041 RepoPathBuf::parse_fs_path(
1042 &cwd_path,
1043 wc_path,
1044 format!("dir{}file", std::path::MAIN_SEPARATOR)
1045 )
1046 .as_deref(),
1047 Ok(repo_path("dir/file"))
1048 );
1049 assert_eq!(
1050 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1051 Ok(repo_path("dir/file"))
1052 );
1053 assert_matches!(
1054 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1055 Err(FsPathParseError {
1056 source: RelativePathParseError::InvalidComponent { .. },
1057 ..
1058 })
1059 );
1060 assert_eq!(
1061 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1062 Ok(RepoPath::root())
1063 );
1064 assert_eq!(
1065 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1066 Ok(repo_path("file"))
1067 );
1068 assert_eq!(
1070 RepoPathBuf::parse_fs_path(
1071 &cwd_path,
1072 &cwd_path,
1073 cwd_path.join("../repo").to_str().unwrap()
1074 )
1075 .as_deref(),
1076 Ok(RepoPath::root())
1077 );
1078 }
1079
1080 #[test]
1081 fn parse_fs_path_wc_in_cwd_parent() {
1082 let temp_dir = new_temp_dir();
1083 let cwd_path = temp_dir.path().join("dir");
1084 let wc_path = cwd_path.parent().unwrap().to_path_buf();
1085
1086 assert_eq!(
1087 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1088 Ok(repo_path("dir"))
1089 );
1090 assert_eq!(
1091 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1092 Ok(repo_path("dir"))
1093 );
1094 assert_eq!(
1095 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1096 Ok(repo_path("dir/file"))
1097 );
1098 assert_eq!(
1099 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1100 Ok(repo_path("dir/subdir/file"))
1101 );
1102 assert_eq!(
1103 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1104 Ok(RepoPath::root())
1105 );
1106 assert_matches!(
1107 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1108 Err(FsPathParseError {
1109 source: RelativePathParseError::InvalidComponent { .. },
1110 ..
1111 })
1112 );
1113 assert_eq!(
1114 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1115 Ok(repo_path("other-dir/file"))
1116 );
1117 }
1118
1119 #[test]
1120 fn parse_fs_path_wc_in_cwd_child() {
1121 let temp_dir = new_temp_dir();
1122 let cwd_path = temp_dir.path().join("cwd");
1123 let wc_path = cwd_path.join("repo");
1124
1125 assert_matches!(
1126 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1127 Err(FsPathParseError {
1128 source: RelativePathParseError::InvalidComponent { .. },
1129 ..
1130 })
1131 );
1132 assert_matches!(
1133 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1134 Err(FsPathParseError {
1135 source: RelativePathParseError::InvalidComponent { .. },
1136 ..
1137 })
1138 );
1139 assert_eq!(
1140 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1141 Ok(RepoPath::root())
1142 );
1143 assert_eq!(
1144 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1145 Ok(repo_path("file"))
1146 );
1147 assert_eq!(
1148 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1149 Ok(repo_path("dir/file"))
1150 );
1151 }
1152
1153 #[test]
1154 fn test_format_copied_path() {
1155 let ui = RepoPathUiConverter::Fs {
1156 cwd: PathBuf::from("."),
1157 base: PathBuf::from("."),
1158 };
1159
1160 let format = |before, after| {
1161 ui.format_copied_path(repo_path(before), repo_path(after))
1162 .replace('\\', "/")
1163 };
1164
1165 assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1166 assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1167 assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1168 assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1169 assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1170 assert_eq!(
1171 format("one/two", "one/two/three/one/two"),
1172 "one/{ => two/three/one}/two"
1173 );
1174
1175 assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1176 assert_eq!(
1177 format("one/two/three", "one/four/three"),
1178 "one/{two => four}/three"
1179 );
1180 assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1181 assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1182 assert_eq!(format("two", "four"), "{two => four}");
1183 assert_eq!(format("file1", "file2"), "{file1 => file2}");
1184 assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1185 }
1186}