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::RefCastCustom;
30use ref_cast::ref_cast_custom;
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<Self> for RepoPathComponent {
118 fn as_ref(&self) -> &Self {
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, serde::Serialize)]
203#[serde(transparent)]
204pub struct RepoPathBuf {
205 value: String,
208}
209
210#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom, serde::Serialize)]
212#[repr(transparent)]
213#[serde(transparent)]
214pub struct RepoPath {
215 value: str,
216}
217
218impl Debug for RepoPath {
219 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
220 write!(f, "{:?}", &self.value)
221 }
222}
223
224impl Debug for RepoPathBuf {
225 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
226 <RepoPath as Debug>::fmt(self, f)
227 }
228}
229
230#[derive(Clone, Debug, Eq, Error, PartialEq)]
234#[error(r#"Invalid repo path input "{value}""#)]
235pub struct InvalidNewRepoPathError {
236 value: String,
237}
238
239impl RepoPathBuf {
240 pub const fn root() -> Self {
242 Self {
243 value: String::new(),
244 }
245 }
246
247 pub fn from_internal_string(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
249 let value: String = value.into();
250 if is_valid_repo_path_str(&value) {
251 Ok(Self { value })
252 } else {
253 Err(InvalidNewRepoPathError { value })
254 }
255 }
256
257 pub fn from_relative_path(
261 relative_path: impl AsRef<Path>,
262 ) -> Result<Self, RelativePathParseError> {
263 let relative_path = relative_path.as_ref();
264 if relative_path == Path::new(".") {
265 return Ok(Self::root());
266 }
267
268 let mut components = relative_path
269 .components()
270 .map(|c| match c {
271 Component::Normal(name) => {
272 name.to_str()
273 .ok_or_else(|| RelativePathParseError::InvalidUtf8 {
274 path: relative_path.into(),
275 })
276 }
277 _ => Err(RelativePathParseError::InvalidComponent {
278 component: c.as_os_str().to_string_lossy().into(),
279 path: relative_path.into(),
280 }),
281 })
282 .fuse();
283 let mut value = String::with_capacity(relative_path.as_os_str().len());
284 if let Some(name) = components.next() {
285 value.push_str(name?);
286 }
287 for name in components {
288 value.push('/');
289 value.push_str(name?);
290 }
291 Ok(Self { value })
292 }
293
294 pub fn parse_fs_path(
300 cwd: &Path,
301 base: &Path,
302 input: impl AsRef<Path>,
303 ) -> Result<Self, FsPathParseError> {
304 let input = input.as_ref();
305 let abs_input_path = file_util::normalize_path(&cwd.join(input));
306 let repo_relative_path = file_util::relative_path(base, &abs_input_path);
307 Self::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError {
308 base: file_util::relative_path(cwd, base).into(),
309 input: input.into(),
310 source,
311 })
312 }
313
314 pub fn into_internal_string(self) -> String {
316 self.value
317 }
318}
319
320impl RepoPath {
321 pub const fn root() -> &'static Self {
323 Self::from_internal_string_unchecked("")
324 }
325
326 pub fn from_internal_string(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
331 if is_valid_repo_path_str(value) {
332 Ok(Self::from_internal_string_unchecked(value))
333 } else {
334 Err(InvalidNewRepoPathError {
335 value: value.to_owned(),
336 })
337 }
338 }
339
340 #[ref_cast_custom]
341 const fn from_internal_string_unchecked(value: &str) -> &Self;
342
343 pub fn to_internal_dir_string(&self) -> String {
348 if self.value.is_empty() {
349 String::new()
350 } else {
351 [&self.value, "/"].concat()
352 }
353 }
354
355 pub fn as_internal_file_string(&self) -> &str {
358 &self.value
359 }
360
361 pub fn to_fs_path(&self, base: &Path) -> Result<PathBuf, InvalidRepoPathError> {
366 let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
367 result.push(base);
368 for c in self.components() {
369 result.push(c.to_fs_name().map_err(|err| err.with_path(self))?);
370 }
371 if result.as_os_str().is_empty() {
372 result.push(".");
373 }
374 Ok(result)
375 }
376
377 pub fn to_fs_path_unchecked(&self, base: &Path) -> PathBuf {
383 let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
384 result.push(base);
385 result.extend(self.components().map(RepoPathComponent::as_internal_str));
386 if result.as_os_str().is_empty() {
387 result.push(".");
388 }
389 result
390 }
391
392 pub fn is_root(&self) -> bool {
393 self.value.is_empty()
394 }
395
396 pub fn starts_with(&self, base: &Self) -> bool {
398 self.strip_prefix(base).is_some()
399 }
400
401 pub fn strip_prefix(&self, base: &Self) -> Option<&Self> {
403 if base.value.is_empty() {
404 Some(self)
405 } else {
406 let tail = self.value.strip_prefix(&base.value)?;
407 if tail.is_empty() {
408 Some(Self::from_internal_string_unchecked(tail))
409 } else {
410 tail.strip_prefix('/')
411 .map(Self::from_internal_string_unchecked)
412 }
413 }
414 }
415
416 pub fn parent(&self) -> Option<&Self> {
418 self.split().map(|(parent, _)| parent)
419 }
420
421 pub fn split(&self) -> Option<(&Self, &RepoPathComponent)> {
423 let mut components = self.components();
424 let basename = components.next_back()?;
425 Some((components.as_path(), basename))
426 }
427
428 pub fn components(&self) -> RepoPathComponentsIter<'_> {
429 RepoPathComponentsIter { value: &self.value }
430 }
431
432 pub fn ancestors(&self) -> impl Iterator<Item = &Self> {
433 std::iter::successors(Some(self), |path| path.parent())
434 }
435
436 pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf {
437 let value = if self.value.is_empty() {
438 entry.as_internal_str().to_owned()
439 } else {
440 [&self.value, "/", entry.as_internal_str()].concat()
441 };
442 RepoPathBuf { value }
443 }
444}
445
446impl AsRef<Self> for RepoPath {
447 fn as_ref(&self) -> &Self {
448 self
449 }
450}
451
452impl AsRef<RepoPath> for RepoPathBuf {
453 fn as_ref(&self) -> &RepoPath {
454 self
455 }
456}
457
458impl Borrow<RepoPath> for RepoPathBuf {
459 fn borrow(&self) -> &RepoPath {
460 self
461 }
462}
463
464impl Deref for RepoPathBuf {
465 type Target = RepoPath;
466
467 fn deref(&self) -> &Self::Target {
468 RepoPath::from_internal_string_unchecked(&self.value)
469 }
470}
471
472impl ToOwned for RepoPath {
473 type Owned = RepoPathBuf;
474
475 fn to_owned(&self) -> Self::Owned {
476 let value = self.value.to_owned();
477 RepoPathBuf { value }
478 }
479
480 fn clone_into(&self, target: &mut Self::Owned) {
481 self.value.clone_into(&mut target.value);
482 }
483}
484
485impl Ord for RepoPath {
486 fn cmp(&self, other: &Self) -> Ordering {
487 debug_assert!(is_valid_repo_path_str(&self.value));
490 self.components().cmp(other.components())
491 }
492}
493
494impl Ord for RepoPathBuf {
495 fn cmp(&self, other: &Self) -> Ordering {
496 <RepoPath as Ord>::cmp(self, other)
497 }
498}
499
500impl PartialOrd for RepoPath {
501 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
502 Some(self.cmp(other))
503 }
504}
505
506impl PartialOrd for RepoPathBuf {
507 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
508 Some(self.cmp(other))
509 }
510}
511
512impl<P: AsRef<RepoPathComponent>> Extend<P> for RepoPathBuf {
513 fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
514 for component in iter {
515 if !self.value.is_empty() {
516 self.value.push('/');
517 }
518 self.value.push_str(component.as_ref().as_internal_str());
519 }
520 }
521}
522
523#[derive(Clone, Debug, Eq, Error, PartialEq)]
525#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())]
526pub struct InvalidRepoPathError {
527 pub path: RepoPathBuf,
529 pub source: InvalidRepoPathComponentError,
531}
532
533#[derive(Clone, Debug, Eq, Error, PartialEq)]
535#[error(r#"Invalid path component "{component}""#)]
536pub struct InvalidRepoPathComponentError {
537 pub component: Box<str>,
538}
539
540impl InvalidRepoPathComponentError {
541 pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError {
543 InvalidRepoPathError {
544 path: path.to_owned(),
545 source: self,
546 }
547 }
548}
549
550#[derive(Clone, Debug, Eq, Error, PartialEq)]
551pub enum RelativePathParseError {
552 #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
553 InvalidComponent {
554 component: Box<str>,
555 path: Box<Path>,
556 },
557 #[error(r#"Not valid UTF-8 path "{path}""#)]
558 InvalidUtf8 { path: Box<Path> },
559}
560
561#[derive(Clone, Debug, Eq, Error, PartialEq)]
562#[error(r#"Path "{input}" is not in the repo "{base}""#)]
563pub struct FsPathParseError {
564 pub base: Box<Path>,
566 pub input: Box<Path>,
568 pub source: RelativePathParseError,
570}
571
572fn is_valid_repo_path_component_str(value: &str) -> bool {
573 !value.is_empty() && !value.contains('/')
574}
575
576fn is_valid_repo_path_str(value: &str) -> bool {
577 !value.starts_with('/') && !value.ends_with('/') && !value.contains("//")
578}
579
580#[derive(Debug, Error)]
582pub enum UiPathParseError {
583 #[error(transparent)]
584 Fs(FsPathParseError),
585}
586
587#[derive(Debug, Clone)]
590pub enum RepoPathUiConverter {
591 Fs { cwd: PathBuf, base: PathBuf },
597 }
600
601impl RepoPathUiConverter {
602 pub fn format_file_path(&self, file: &RepoPath) -> String {
604 match self {
605 Self::Fs { cwd, base } => {
606 file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
607 .display()
608 .to_string()
609 }
610 }
611 }
612
613 pub fn format_copied_path(&self, source: &RepoPath, target: &RepoPath) -> String {
619 if source == target {
620 return self.format_file_path(source);
621 }
622 let mut formatted = String::new();
623 match self {
624 Self::Fs { cwd, base } => {
625 let source_path = file_util::relative_path(cwd, &source.to_fs_path_unchecked(base));
626 let target_path = file_util::relative_path(cwd, &target.to_fs_path_unchecked(base));
627
628 let source_components = source_path.components().collect_vec();
629 let target_components = target_path.components().collect_vec();
630
631 let prefix_count = source_components
632 .iter()
633 .zip(target_components.iter())
634 .take_while(|(source_component, target_component)| {
635 source_component == target_component
636 })
637 .count()
638 .min(source_components.len().saturating_sub(1))
639 .min(target_components.len().saturating_sub(1));
640
641 let suffix_count = source_components
642 .iter()
643 .skip(prefix_count)
644 .rev()
645 .zip(target_components.iter().skip(prefix_count).rev())
646 .take_while(|(source_component, target_component)| {
647 source_component == target_component
648 })
649 .count()
650 .min(source_components.len().saturating_sub(1))
651 .min(target_components.len().saturating_sub(1));
652
653 fn format_components(c: &[std::path::Component]) -> String {
654 c.iter().collect::<PathBuf>().display().to_string()
655 }
656
657 if prefix_count > 0 {
658 formatted.push_str(&format_components(&source_components[0..prefix_count]));
659 formatted.push_str(std::path::MAIN_SEPARATOR_STR);
660 }
661 formatted.push('{');
662 formatted.push_str(&format_components(
663 &source_components
664 [prefix_count..(source_components.len() - suffix_count).max(prefix_count)],
665 ));
666 formatted.push_str(" => ");
667 formatted.push_str(&format_components(
668 &target_components
669 [prefix_count..(target_components.len() - suffix_count).max(prefix_count)],
670 ));
671 formatted.push('}');
672 if suffix_count > 0 {
673 formatted.push_str(std::path::MAIN_SEPARATOR_STR);
674 formatted.push_str(&format_components(
675 &source_components[source_components.len() - suffix_count..],
676 ));
677 }
678 }
679 }
680 formatted
681 }
682
683 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
688 match self {
689 Self::Fs { cwd, base } => {
690 RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
691 }
692 }
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use std::panic;
699
700 use assert_matches::assert_matches;
701 use itertools::Itertools as _;
702
703 use super::*;
704 use crate::tests::new_temp_dir;
705
706 fn repo_path(value: &str) -> &RepoPath {
707 RepoPath::from_internal_string(value).unwrap()
708 }
709
710 fn repo_path_component(value: &str) -> &RepoPathComponent {
711 RepoPathComponent::new(value).unwrap()
712 }
713
714 #[test]
715 fn test_is_root() {
716 assert!(RepoPath::root().is_root());
717 assert!(repo_path("").is_root());
718 assert!(!repo_path("foo").is_root());
719 }
720
721 #[test]
722 fn test_from_internal_string() {
723 let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
724 assert_eq!(repo_path_buf(""), RepoPathBuf::root());
725 assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
726 assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
727 assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
728 assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
729
730 assert_eq!(repo_path(""), RepoPath::root());
731 assert!(panic::catch_unwind(|| repo_path("/")).is_err());
732 assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
733 assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
734 assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
735 }
736
737 #[test]
738 fn test_as_internal_file_string() {
739 assert_eq!(RepoPath::root().as_internal_file_string(), "");
740 assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
741 assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
742 }
743
744 #[test]
745 fn test_to_internal_dir_string() {
746 assert_eq!(RepoPath::root().to_internal_dir_string(), "");
747 assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
748 assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
749 }
750
751 #[test]
752 fn test_starts_with() {
753 assert!(repo_path("").starts_with(repo_path("")));
754 assert!(repo_path("x").starts_with(repo_path("")));
755 assert!(!repo_path("").starts_with(repo_path("x")));
756
757 assert!(repo_path("x").starts_with(repo_path("x")));
758 assert!(repo_path("x/y").starts_with(repo_path("x")));
759 assert!(!repo_path("xy").starts_with(repo_path("x")));
760 assert!(!repo_path("x/y").starts_with(repo_path("y")));
761
762 assert!(repo_path("x/y").starts_with(repo_path("x/y")));
763 assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
764 assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
765 assert!(!repo_path("x").starts_with(repo_path("x/y")));
766 assert!(!repo_path("xy").starts_with(repo_path("x/y")));
767 }
768
769 #[test]
770 fn test_strip_prefix() {
771 assert_eq!(
772 repo_path("").strip_prefix(repo_path("")),
773 Some(repo_path(""))
774 );
775 assert_eq!(
776 repo_path("x").strip_prefix(repo_path("")),
777 Some(repo_path("x"))
778 );
779 assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
780
781 assert_eq!(
782 repo_path("x").strip_prefix(repo_path("x")),
783 Some(repo_path(""))
784 );
785 assert_eq!(
786 repo_path("x/y").strip_prefix(repo_path("x")),
787 Some(repo_path("y"))
788 );
789 assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
790 assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
791
792 assert_eq!(
793 repo_path("x/y").strip_prefix(repo_path("x/y")),
794 Some(repo_path(""))
795 );
796 assert_eq!(
797 repo_path("x/y/z").strip_prefix(repo_path("x/y")),
798 Some(repo_path("z"))
799 );
800 assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
801 assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
802 assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
803 }
804
805 #[test]
806 fn test_order() {
807 assert!(RepoPath::root() < repo_path("dir"));
808 assert!(repo_path("dir") < repo_path("dirx"));
809 assert!(repo_path("dir") < repo_path("dir#"));
811 assert!(repo_path("dir") < repo_path("dir/sub"));
812 assert!(repo_path("dir/sub") < repo_path("dir#"));
813
814 assert!(repo_path("abc") < repo_path("dir/file"));
815 assert!(repo_path("dir") < repo_path("dir/file"));
816 assert!(repo_path("dis") > repo_path("dir/file"));
817 assert!(repo_path("xyz") > repo_path("dir/file"));
818 assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
819 }
820
821 #[test]
822 fn test_join() {
823 let root = RepoPath::root();
824 let dir = root.join(repo_path_component("dir"));
825 assert_eq!(dir.as_ref(), repo_path("dir"));
826 let subdir = dir.join(repo_path_component("subdir"));
827 assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
828 assert_eq!(
829 subdir.join(repo_path_component("file")).as_ref(),
830 repo_path("dir/subdir/file")
831 );
832 }
833
834 #[test]
835 fn test_extend() {
836 let mut path = RepoPathBuf::root();
837 path.extend(std::iter::empty::<RepoPathComponentBuf>());
838 assert_eq!(path.as_ref(), RepoPath::root());
839 path.extend([repo_path_component("dir")]);
840 assert_eq!(path.as_ref(), repo_path("dir"));
841 path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
842 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
843 path.extend(std::iter::empty::<RepoPathComponentBuf>());
844 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
845 }
846
847 #[test]
848 fn test_parent() {
849 let root = RepoPath::root();
850 let dir_component = repo_path_component("dir");
851 let subdir_component = repo_path_component("subdir");
852
853 let dir = root.join(dir_component);
854 let subdir = dir.join(subdir_component);
855
856 assert_eq!(root.parent(), None);
857 assert_eq!(dir.parent(), Some(root));
858 assert_eq!(subdir.parent(), Some(dir.as_ref()));
859 }
860
861 #[test]
862 fn test_split() {
863 let root = RepoPath::root();
864 let dir_component = repo_path_component("dir");
865 let file_component = repo_path_component("file");
866
867 let dir = root.join(dir_component);
868 let file = dir.join(file_component);
869
870 assert_eq!(root.split(), None);
871 assert_eq!(dir.split(), Some((root, dir_component)));
872 assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
873 }
874
875 #[test]
876 fn test_components() {
877 assert!(RepoPath::root().components().next().is_none());
878 assert_eq!(
879 repo_path("dir").components().collect_vec(),
880 vec![repo_path_component("dir")]
881 );
882 assert_eq!(
883 repo_path("dir/subdir").components().collect_vec(),
884 vec![repo_path_component("dir"), repo_path_component("subdir")]
885 );
886
887 assert!(RepoPath::root().components().next_back().is_none());
889 assert_eq!(
890 repo_path("dir").components().rev().collect_vec(),
891 vec![repo_path_component("dir")]
892 );
893 assert_eq!(
894 repo_path("dir/subdir").components().rev().collect_vec(),
895 vec![repo_path_component("subdir"), repo_path_component("dir")]
896 );
897 }
898
899 #[test]
900 fn test_ancestors() {
901 assert_eq!(
902 RepoPath::root().ancestors().collect_vec(),
903 vec![RepoPath::root()]
904 );
905 assert_eq!(
906 repo_path("dir").ancestors().collect_vec(),
907 vec![repo_path("dir"), RepoPath::root()]
908 );
909 assert_eq!(
910 repo_path("dir/subdir").ancestors().collect_vec(),
911 vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
912 );
913 }
914
915 #[test]
916 fn test_to_fs_path() {
917 assert_eq!(
918 repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
919 Path::new("base/dir")
920 );
921 assert_eq!(
922 repo_path("").to_fs_path(Path::new("")).unwrap(),
923 Path::new(".")
924 );
925 assert_eq!(
926 repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
927 Path::new("base/dir/file")
928 );
929 assert_eq!(
930 repo_path("some/deep/dir/file")
931 .to_fs_path(Path::new("base/dir"))
932 .unwrap(),
933 Path::new("base/dir/some/deep/dir/file")
934 );
935 assert_eq!(
936 repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
937 Path::new("dir/file")
938 );
939
940 assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
942 assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
943 assert!(
944 repo_path("dir/../file")
945 .to_fs_path(Path::new("base"))
946 .is_err()
947 );
948 assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
949 assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
950 assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
951 assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
952
953 assert!(
955 RepoPath::from_internal_string_unchecked("/")
956 .to_fs_path(Path::new("base"))
957 .is_err()
958 );
959 assert_eq!(
960 RepoPath::from_internal_string_unchecked("a/")
963 .to_fs_path(Path::new("base"))
964 .unwrap(),
965 Path::new("base/a")
966 );
967 assert!(
968 RepoPath::from_internal_string_unchecked("/b")
969 .to_fs_path(Path::new("base"))
970 .is_err()
971 );
972 assert!(
973 RepoPath::from_internal_string_unchecked("a//b")
974 .to_fs_path(Path::new("base"))
975 .is_err()
976 );
977
978 assert!(
980 RepoPathComponent::new_unchecked("wind/ows")
981 .to_fs_name()
982 .is_err()
983 );
984 assert!(
985 RepoPathComponent::new_unchecked("./file")
986 .to_fs_name()
987 .is_err()
988 );
989 assert!(
990 RepoPathComponent::new_unchecked("file/.")
991 .to_fs_name()
992 .is_err()
993 );
994 assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
995
996 if cfg!(windows) {
998 assert!(
999 repo_path(r#"wind\ows"#)
1000 .to_fs_path(Path::new("base"))
1001 .is_err()
1002 );
1003 assert!(
1004 repo_path(r#".\file"#)
1005 .to_fs_path(Path::new("base"))
1006 .is_err()
1007 );
1008 assert!(
1009 repo_path(r#"file\."#)
1010 .to_fs_path(Path::new("base"))
1011 .is_err()
1012 );
1013 assert!(
1014 repo_path(r#"c:/foo"#)
1015 .to_fs_path(Path::new("base"))
1016 .is_err()
1017 );
1018 }
1019 }
1020
1021 #[test]
1022 fn test_to_fs_path_unchecked() {
1023 assert_eq!(
1024 repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1025 Path::new("base/dir")
1026 );
1027 assert_eq!(
1028 repo_path("").to_fs_path_unchecked(Path::new("")),
1029 Path::new(".")
1030 );
1031 assert_eq!(
1032 repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1033 Path::new("base/dir/file")
1034 );
1035 assert_eq!(
1036 repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1037 Path::new("base/dir/some/deep/dir/file")
1038 );
1039 assert_eq!(
1040 repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1041 Path::new("dir/file")
1042 );
1043 }
1044
1045 #[test]
1046 fn parse_fs_path_wc_in_cwd() {
1047 let temp_dir = new_temp_dir();
1048 let cwd_path = temp_dir.path().join("repo");
1049 let wc_path = &cwd_path;
1050
1051 assert_eq!(
1052 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1053 Ok(RepoPath::root())
1054 );
1055 assert_eq!(
1056 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1057 Ok(RepoPath::root())
1058 );
1059 assert_eq!(
1060 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1061 Ok(repo_path("file"))
1062 );
1063 assert_eq!(
1065 RepoPathBuf::parse_fs_path(
1066 &cwd_path,
1067 wc_path,
1068 format!("dir{}file", std::path::MAIN_SEPARATOR)
1069 )
1070 .as_deref(),
1071 Ok(repo_path("dir/file"))
1072 );
1073 assert_eq!(
1074 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1075 Ok(repo_path("dir/file"))
1076 );
1077 assert_matches!(
1078 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1079 Err(FsPathParseError {
1080 source: RelativePathParseError::InvalidComponent { .. },
1081 ..
1082 })
1083 );
1084 assert_eq!(
1085 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1086 Ok(RepoPath::root())
1087 );
1088 assert_eq!(
1089 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1090 Ok(repo_path("file"))
1091 );
1092 assert_eq!(
1094 RepoPathBuf::parse_fs_path(
1095 &cwd_path,
1096 &cwd_path,
1097 cwd_path.join("../repo").to_str().unwrap()
1098 )
1099 .as_deref(),
1100 Ok(RepoPath::root())
1101 );
1102 }
1103
1104 #[test]
1105 fn parse_fs_path_wc_in_cwd_parent() {
1106 let temp_dir = new_temp_dir();
1107 let cwd_path = temp_dir.path().join("dir");
1108 let wc_path = cwd_path.parent().unwrap().to_path_buf();
1109
1110 assert_eq!(
1111 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1112 Ok(repo_path("dir"))
1113 );
1114 assert_eq!(
1115 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1116 Ok(repo_path("dir"))
1117 );
1118 assert_eq!(
1119 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1120 Ok(repo_path("dir/file"))
1121 );
1122 assert_eq!(
1123 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1124 Ok(repo_path("dir/subdir/file"))
1125 );
1126 assert_eq!(
1127 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1128 Ok(RepoPath::root())
1129 );
1130 assert_matches!(
1131 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1132 Err(FsPathParseError {
1133 source: RelativePathParseError::InvalidComponent { .. },
1134 ..
1135 })
1136 );
1137 assert_eq!(
1138 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1139 Ok(repo_path("other-dir/file"))
1140 );
1141 }
1142
1143 #[test]
1144 fn parse_fs_path_wc_in_cwd_child() {
1145 let temp_dir = new_temp_dir();
1146 let cwd_path = temp_dir.path().join("cwd");
1147 let wc_path = cwd_path.join("repo");
1148
1149 assert_matches!(
1150 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1151 Err(FsPathParseError {
1152 source: RelativePathParseError::InvalidComponent { .. },
1153 ..
1154 })
1155 );
1156 assert_matches!(
1157 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1158 Err(FsPathParseError {
1159 source: RelativePathParseError::InvalidComponent { .. },
1160 ..
1161 })
1162 );
1163 assert_eq!(
1164 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1165 Ok(RepoPath::root())
1166 );
1167 assert_eq!(
1168 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1169 Ok(repo_path("file"))
1170 );
1171 assert_eq!(
1172 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1173 Ok(repo_path("dir/file"))
1174 );
1175 }
1176
1177 #[test]
1178 fn test_format_copied_path() {
1179 let ui = RepoPathUiConverter::Fs {
1180 cwd: PathBuf::from("."),
1181 base: PathBuf::from("."),
1182 };
1183
1184 let format = |before, after| {
1185 ui.format_copied_path(repo_path(before), repo_path(after))
1186 .replace('\\', "/")
1187 };
1188
1189 assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1190 assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1191 assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1192 assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1193 assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1194 assert_eq!(
1195 format("one/two", "one/two/three/one/two"),
1196 "one/{ => two/three/one}/two"
1197 );
1198
1199 assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1200 assert_eq!(
1201 format("one/two/three", "one/four/three"),
1202 "one/{two => four}/three"
1203 );
1204 assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1205 assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1206 assert_eq!(format("two", "four"), "{two => four}");
1207 assert_eq!(format("file1", "file2"), "{file1 => file2}");
1208 assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1209 assert_eq!(
1210 format("x/something/something/2to1.txt", "x/something/2to1.txt"),
1211 "x/something/{something => }/2to1.txt"
1212 );
1213 assert_eq!(
1214 format("x/something/1to2.txt", "x/something/something/1to2.txt"),
1215 "x/something/{ => something}/1to2.txt"
1216 );
1217 }
1218}