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, 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 RepoPathBuf {
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(RepoPathBuf { 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(RepoPathBuf { 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: &RepoPath) -> bool {
398 self.strip_prefix(base).is_some()
399 }
400
401 pub fn strip_prefix(&self, base: &RepoPath) -> Option<&RepoPath> {
403 if base.value.is_empty() {
404 Some(self)
405 } else {
406 let tail = self.value.strip_prefix(&base.value)?;
407 if tail.is_empty() {
408 Some(RepoPath::from_internal_string_unchecked(tail))
409 } else {
410 tail.strip_prefix('/')
411 .map(RepoPath::from_internal_string_unchecked)
412 }
413 }
414 }
415
416 pub fn parent(&self) -> Option<&RepoPath> {
418 self.split().map(|(parent, _)| parent)
419 }
420
421 pub fn split(&self) -> Option<(&RepoPath, &RepoPathComponent)> {
423 let mut components = self.components();
424 let basename = components.next_back()?;
425 Some((components.as_path(), basename))
426 }
427
428 pub fn components(&self) -> RepoPathComponentsIter<'_> {
429 RepoPathComponentsIter { value: &self.value }
430 }
431
432 pub fn ancestors(&self) -> impl Iterator<Item = &RepoPath> {
433 std::iter::successors(Some(self), |path| path.parent())
434 }
435
436 pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf {
437 let value = if self.value.is_empty() {
438 entry.as_internal_str().to_owned()
439 } else {
440 [&self.value, "/", entry.as_internal_str()].concat()
441 };
442 RepoPathBuf { value }
443 }
444}
445
446impl AsRef<RepoPath> for RepoPath {
447 fn as_ref(&self) -> &RepoPath {
448 self
449 }
450}
451
452impl AsRef<RepoPath> for RepoPathBuf {
453 fn as_ref(&self) -> &RepoPath {
454 self
455 }
456}
457
458impl Borrow<RepoPath> for RepoPathBuf {
459 fn borrow(&self) -> &RepoPath {
460 self
461 }
462}
463
464impl Deref for RepoPathBuf {
465 type Target = RepoPath;
466
467 fn deref(&self) -> &Self::Target {
468 RepoPath::from_internal_string_unchecked(&self.value)
469 }
470}
471
472impl ToOwned for RepoPath {
473 type Owned = RepoPathBuf;
474
475 fn to_owned(&self) -> Self::Owned {
476 let value = self.value.to_owned();
477 RepoPathBuf { value }
478 }
479
480 fn clone_into(&self, target: &mut Self::Owned) {
481 self.value.clone_into(&mut target.value);
482 }
483}
484
485impl Ord for RepoPath {
486 fn cmp(&self, other: &Self) -> Ordering {
487 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 RepoPathUiConverter::Fs { cwd, base } => {
606 file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
607 .display()
608 .to_string()
609 }
610 }
611 }
612
613 pub fn format_copied_path(&self, source: &RepoPath, target: &RepoPath) -> String {
619 if source == target {
620 return self.format_file_path(source);
621 }
622 let mut formatted = String::new();
623 match self {
624 RepoPathUiConverter::Fs { cwd, base } => {
625 let source_path = file_util::relative_path(cwd, &source.to_fs_path_unchecked(base));
626 let target_path = file_util::relative_path(cwd, &target.to_fs_path_unchecked(base));
627
628 let source_components = source_path.components().collect_vec();
629 let target_components = target_path.components().collect_vec();
630
631 let prefix_count = source_components
632 .iter()
633 .zip(target_components.iter())
634 .take_while(|(source_component, target_component)| {
635 source_component == target_component
636 })
637 .count()
638 .min(source_components.len().saturating_sub(1))
639 .min(target_components.len().saturating_sub(1));
640
641 let suffix_count = source_components
642 .iter()
643 .rev()
644 .zip(target_components.iter().rev())
645 .take_while(|(source_component, target_component)| {
646 source_component == target_component
647 })
648 .count()
649 .min(source_components.len().saturating_sub(1))
650 .min(target_components.len().saturating_sub(1));
651
652 fn format_components(c: &[std::path::Component]) -> String {
653 c.iter().collect::<PathBuf>().display().to_string()
654 }
655
656 if prefix_count > 0 {
657 formatted.push_str(&format_components(&source_components[0..prefix_count]));
658 formatted.push_str(std::path::MAIN_SEPARATOR_STR);
659 }
660 formatted.push('{');
661 formatted.push_str(&format_components(
662 &source_components
663 [prefix_count..(source_components.len() - suffix_count).max(prefix_count)],
664 ));
665 formatted.push_str(" => ");
666 formatted.push_str(&format_components(
667 &target_components
668 [prefix_count..(target_components.len() - suffix_count).max(prefix_count)],
669 ));
670 formatted.push('}');
671 if suffix_count > 0 {
672 formatted.push_str(std::path::MAIN_SEPARATOR_STR);
673 formatted.push_str(&format_components(
674 &source_components[source_components.len() - suffix_count..],
675 ));
676 }
677 }
678 }
679 formatted
680 }
681
682 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
687 match self {
688 RepoPathUiConverter::Fs { cwd, base } => {
689 RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
690 }
691 }
692 }
693}
694
695#[cfg(test)]
696mod tests {
697 use std::panic;
698
699 use assert_matches::assert_matches;
700 use itertools::Itertools as _;
701
702 use super::*;
703 use crate::tests::new_temp_dir;
704
705 fn repo_path(value: &str) -> &RepoPath {
706 RepoPath::from_internal_string(value).unwrap()
707 }
708
709 fn repo_path_component(value: &str) -> &RepoPathComponent {
710 RepoPathComponent::new(value).unwrap()
711 }
712
713 #[test]
714 fn test_is_root() {
715 assert!(RepoPath::root().is_root());
716 assert!(repo_path("").is_root());
717 assert!(!repo_path("foo").is_root());
718 }
719
720 #[test]
721 fn test_from_internal_string() {
722 let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
723 assert_eq!(repo_path_buf(""), RepoPathBuf::root());
724 assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
725 assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
726 assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
727 assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
728
729 assert_eq!(repo_path(""), RepoPath::root());
730 assert!(panic::catch_unwind(|| repo_path("/")).is_err());
731 assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
732 assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
733 assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
734 }
735
736 #[test]
737 fn test_as_internal_file_string() {
738 assert_eq!(RepoPath::root().as_internal_file_string(), "");
739 assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
740 assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
741 }
742
743 #[test]
744 fn test_to_internal_dir_string() {
745 assert_eq!(RepoPath::root().to_internal_dir_string(), "");
746 assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
747 assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
748 }
749
750 #[test]
751 fn test_starts_with() {
752 assert!(repo_path("").starts_with(repo_path("")));
753 assert!(repo_path("x").starts_with(repo_path("")));
754 assert!(!repo_path("").starts_with(repo_path("x")));
755
756 assert!(repo_path("x").starts_with(repo_path("x")));
757 assert!(repo_path("x/y").starts_with(repo_path("x")));
758 assert!(!repo_path("xy").starts_with(repo_path("x")));
759 assert!(!repo_path("x/y").starts_with(repo_path("y")));
760
761 assert!(repo_path("x/y").starts_with(repo_path("x/y")));
762 assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
763 assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
764 assert!(!repo_path("x").starts_with(repo_path("x/y")));
765 assert!(!repo_path("xy").starts_with(repo_path("x/y")));
766 }
767
768 #[test]
769 fn test_strip_prefix() {
770 assert_eq!(
771 repo_path("").strip_prefix(repo_path("")),
772 Some(repo_path(""))
773 );
774 assert_eq!(
775 repo_path("x").strip_prefix(repo_path("")),
776 Some(repo_path("x"))
777 );
778 assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
779
780 assert_eq!(
781 repo_path("x").strip_prefix(repo_path("x")),
782 Some(repo_path(""))
783 );
784 assert_eq!(
785 repo_path("x/y").strip_prefix(repo_path("x")),
786 Some(repo_path("y"))
787 );
788 assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
789 assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
790
791 assert_eq!(
792 repo_path("x/y").strip_prefix(repo_path("x/y")),
793 Some(repo_path(""))
794 );
795 assert_eq!(
796 repo_path("x/y/z").strip_prefix(repo_path("x/y")),
797 Some(repo_path("z"))
798 );
799 assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
800 assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
801 assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
802 }
803
804 #[test]
805 fn test_order() {
806 assert!(RepoPath::root() < repo_path("dir"));
807 assert!(repo_path("dir") < repo_path("dirx"));
808 assert!(repo_path("dir") < repo_path("dir#"));
810 assert!(repo_path("dir") < repo_path("dir/sub"));
811 assert!(repo_path("dir/sub") < repo_path("dir#"));
812
813 assert!(repo_path("abc") < repo_path("dir/file"));
814 assert!(repo_path("dir") < repo_path("dir/file"));
815 assert!(repo_path("dis") > repo_path("dir/file"));
816 assert!(repo_path("xyz") > repo_path("dir/file"));
817 assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
818 }
819
820 #[test]
821 fn test_join() {
822 let root = RepoPath::root();
823 let dir = root.join(repo_path_component("dir"));
824 assert_eq!(dir.as_ref(), repo_path("dir"));
825 let subdir = dir.join(repo_path_component("subdir"));
826 assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
827 assert_eq!(
828 subdir.join(repo_path_component("file")).as_ref(),
829 repo_path("dir/subdir/file")
830 );
831 }
832
833 #[test]
834 fn test_extend() {
835 let mut path = RepoPathBuf::root();
836 path.extend(std::iter::empty::<RepoPathComponentBuf>());
837 assert_eq!(path.as_ref(), RepoPath::root());
838 path.extend([repo_path_component("dir")]);
839 assert_eq!(path.as_ref(), repo_path("dir"));
840 path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
841 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
842 path.extend(std::iter::empty::<RepoPathComponentBuf>());
843 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
844 }
845
846 #[test]
847 fn test_parent() {
848 let root = RepoPath::root();
849 let dir_component = repo_path_component("dir");
850 let subdir_component = repo_path_component("subdir");
851
852 let dir = root.join(dir_component);
853 let subdir = dir.join(subdir_component);
854
855 assert_eq!(root.parent(), None);
856 assert_eq!(dir.parent(), Some(root));
857 assert_eq!(subdir.parent(), Some(dir.as_ref()));
858 }
859
860 #[test]
861 fn test_split() {
862 let root = RepoPath::root();
863 let dir_component = repo_path_component("dir");
864 let file_component = repo_path_component("file");
865
866 let dir = root.join(dir_component);
867 let file = dir.join(file_component);
868
869 assert_eq!(root.split(), None);
870 assert_eq!(dir.split(), Some((root, dir_component)));
871 assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
872 }
873
874 #[test]
875 fn test_components() {
876 assert!(RepoPath::root().components().next().is_none());
877 assert_eq!(
878 repo_path("dir").components().collect_vec(),
879 vec![repo_path_component("dir")]
880 );
881 assert_eq!(
882 repo_path("dir/subdir").components().collect_vec(),
883 vec![repo_path_component("dir"), repo_path_component("subdir")]
884 );
885
886 assert!(RepoPath::root().components().next_back().is_none());
888 assert_eq!(
889 repo_path("dir").components().rev().collect_vec(),
890 vec![repo_path_component("dir")]
891 );
892 assert_eq!(
893 repo_path("dir/subdir").components().rev().collect_vec(),
894 vec![repo_path_component("subdir"), repo_path_component("dir")]
895 );
896 }
897
898 #[test]
899 fn test_ancestors() {
900 assert_eq!(
901 RepoPath::root().ancestors().collect_vec(),
902 vec![RepoPath::root()]
903 );
904 assert_eq!(
905 repo_path("dir").ancestors().collect_vec(),
906 vec![repo_path("dir"), RepoPath::root()]
907 );
908 assert_eq!(
909 repo_path("dir/subdir").ancestors().collect_vec(),
910 vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
911 );
912 }
913
914 #[test]
915 fn test_to_fs_path() {
916 assert_eq!(
917 repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
918 Path::new("base/dir")
919 );
920 assert_eq!(
921 repo_path("").to_fs_path(Path::new("")).unwrap(),
922 Path::new(".")
923 );
924 assert_eq!(
925 repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
926 Path::new("base/dir/file")
927 );
928 assert_eq!(
929 repo_path("some/deep/dir/file")
930 .to_fs_path(Path::new("base/dir"))
931 .unwrap(),
932 Path::new("base/dir/some/deep/dir/file")
933 );
934 assert_eq!(
935 repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
936 Path::new("dir/file")
937 );
938
939 assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
941 assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
942 assert!(repo_path("dir/../file")
943 .to_fs_path(Path::new("base"))
944 .is_err());
945 assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
946 assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
947 assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
948 assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
949
950 assert!(RepoPath::from_internal_string_unchecked("/")
952 .to_fs_path(Path::new("base"))
953 .is_err());
954 assert_eq!(
955 RepoPath::from_internal_string_unchecked("a/")
958 .to_fs_path(Path::new("base"))
959 .unwrap(),
960 Path::new("base/a")
961 );
962 assert!(RepoPath::from_internal_string_unchecked("/b")
963 .to_fs_path(Path::new("base"))
964 .is_err());
965 assert!(RepoPath::from_internal_string_unchecked("a//b")
966 .to_fs_path(Path::new("base"))
967 .is_err());
968
969 assert!(RepoPathComponent::new_unchecked("wind/ows")
971 .to_fs_name()
972 .is_err());
973 assert!(RepoPathComponent::new_unchecked("./file")
974 .to_fs_name()
975 .is_err());
976 assert!(RepoPathComponent::new_unchecked("file/.")
977 .to_fs_name()
978 .is_err());
979 assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
980
981 if cfg!(windows) {
983 assert!(repo_path(r#"wind\ows"#)
984 .to_fs_path(Path::new("base"))
985 .is_err());
986 assert!(repo_path(r#".\file"#)
987 .to_fs_path(Path::new("base"))
988 .is_err());
989 assert!(repo_path(r#"file\."#)
990 .to_fs_path(Path::new("base"))
991 .is_err());
992 assert!(repo_path(r#"c:/foo"#)
993 .to_fs_path(Path::new("base"))
994 .is_err());
995 }
996 }
997
998 #[test]
999 fn test_to_fs_path_unchecked() {
1000 assert_eq!(
1001 repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1002 Path::new("base/dir")
1003 );
1004 assert_eq!(
1005 repo_path("").to_fs_path_unchecked(Path::new("")),
1006 Path::new(".")
1007 );
1008 assert_eq!(
1009 repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1010 Path::new("base/dir/file")
1011 );
1012 assert_eq!(
1013 repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1014 Path::new("base/dir/some/deep/dir/file")
1015 );
1016 assert_eq!(
1017 repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1018 Path::new("dir/file")
1019 );
1020 }
1021
1022 #[test]
1023 fn parse_fs_path_wc_in_cwd() {
1024 let temp_dir = new_temp_dir();
1025 let cwd_path = temp_dir.path().join("repo");
1026 let wc_path = &cwd_path;
1027
1028 assert_eq!(
1029 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1030 Ok(RepoPath::root())
1031 );
1032 assert_eq!(
1033 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1034 Ok(RepoPath::root())
1035 );
1036 assert_eq!(
1037 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1038 Ok(repo_path("file"))
1039 );
1040 assert_eq!(
1042 RepoPathBuf::parse_fs_path(
1043 &cwd_path,
1044 wc_path,
1045 format!("dir{}file", std::path::MAIN_SEPARATOR)
1046 )
1047 .as_deref(),
1048 Ok(repo_path("dir/file"))
1049 );
1050 assert_eq!(
1051 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1052 Ok(repo_path("dir/file"))
1053 );
1054 assert_matches!(
1055 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1056 Err(FsPathParseError {
1057 source: RelativePathParseError::InvalidComponent { .. },
1058 ..
1059 })
1060 );
1061 assert_eq!(
1062 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1063 Ok(RepoPath::root())
1064 );
1065 assert_eq!(
1066 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1067 Ok(repo_path("file"))
1068 );
1069 assert_eq!(
1071 RepoPathBuf::parse_fs_path(
1072 &cwd_path,
1073 &cwd_path,
1074 cwd_path.join("../repo").to_str().unwrap()
1075 )
1076 .as_deref(),
1077 Ok(RepoPath::root())
1078 );
1079 }
1080
1081 #[test]
1082 fn parse_fs_path_wc_in_cwd_parent() {
1083 let temp_dir = new_temp_dir();
1084 let cwd_path = temp_dir.path().join("dir");
1085 let wc_path = cwd_path.parent().unwrap().to_path_buf();
1086
1087 assert_eq!(
1088 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1089 Ok(repo_path("dir"))
1090 );
1091 assert_eq!(
1092 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1093 Ok(repo_path("dir"))
1094 );
1095 assert_eq!(
1096 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1097 Ok(repo_path("dir/file"))
1098 );
1099 assert_eq!(
1100 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1101 Ok(repo_path("dir/subdir/file"))
1102 );
1103 assert_eq!(
1104 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1105 Ok(RepoPath::root())
1106 );
1107 assert_matches!(
1108 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1109 Err(FsPathParseError {
1110 source: RelativePathParseError::InvalidComponent { .. },
1111 ..
1112 })
1113 );
1114 assert_eq!(
1115 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1116 Ok(repo_path("other-dir/file"))
1117 );
1118 }
1119
1120 #[test]
1121 fn parse_fs_path_wc_in_cwd_child() {
1122 let temp_dir = new_temp_dir();
1123 let cwd_path = temp_dir.path().join("cwd");
1124 let wc_path = cwd_path.join("repo");
1125
1126 assert_matches!(
1127 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1128 Err(FsPathParseError {
1129 source: RelativePathParseError::InvalidComponent { .. },
1130 ..
1131 })
1132 );
1133 assert_matches!(
1134 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1135 Err(FsPathParseError {
1136 source: RelativePathParseError::InvalidComponent { .. },
1137 ..
1138 })
1139 );
1140 assert_eq!(
1141 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1142 Ok(RepoPath::root())
1143 );
1144 assert_eq!(
1145 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1146 Ok(repo_path("file"))
1147 );
1148 assert_eq!(
1149 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1150 Ok(repo_path("dir/file"))
1151 );
1152 }
1153
1154 #[test]
1155 fn test_format_copied_path() {
1156 let ui = RepoPathUiConverter::Fs {
1157 cwd: PathBuf::from("."),
1158 base: PathBuf::from("."),
1159 };
1160
1161 let format = |before, after| {
1162 ui.format_copied_path(repo_path(before), repo_path(after))
1163 .replace('\\', "/")
1164 };
1165
1166 assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1167 assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1168 assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1169 assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1170 assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1171 assert_eq!(
1172 format("one/two", "one/two/three/one/two"),
1173 "one/{ => two/three/one}/two"
1174 );
1175
1176 assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1177 assert_eq!(
1178 format("one/two/three", "one/four/three"),
1179 "one/{two => four}/three"
1180 );
1181 assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1182 assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1183 assert_eq!(format("two", "four"), "{two => four}");
1184 assert_eq!(format("file1", "file2"), "{file1 => file2}");
1185 assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1186 }
1187}