1#![expect(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 pub fn split_common_prefix(&self, other: &Self) -> (&Self, &Self) {
479 let mut prefix_len = 0;
481
482 let common_components = self
483 .components()
484 .zip(other.components())
485 .take_while(|(prev_comp, this_comp)| prev_comp == this_comp);
486
487 for (self_comp, _other_comp) in common_components {
488 if prefix_len > 0 {
489 prefix_len += 1;
493 }
494
495 prefix_len += self_comp.value.len();
496 }
497
498 if prefix_len == 0 {
499 return (Self::root(), self);
501 }
502
503 if prefix_len == self.value.len() {
504 return (self, Self::root());
505 }
506
507 let common_prefix = Self::from_internal_string_unchecked(&self.value[..prefix_len]);
508 let remainder = Self::from_internal_string_unchecked(&self.value[prefix_len + 1..]);
509
510 (common_prefix, remainder)
511 }
512}
513
514impl AsRef<Self> for RepoPath {
515 fn as_ref(&self) -> &Self {
516 self
517 }
518}
519
520impl AsRef<RepoPath> for RepoPathBuf {
521 fn as_ref(&self) -> &RepoPath {
522 self
523 }
524}
525
526impl Borrow<RepoPath> for RepoPathBuf {
527 fn borrow(&self) -> &RepoPath {
528 self
529 }
530}
531
532impl Deref for RepoPathBuf {
533 type Target = RepoPath;
534
535 fn deref(&self) -> &Self::Target {
536 RepoPath::from_internal_string_unchecked(&self.value)
537 }
538}
539
540impl ToOwned for RepoPath {
541 type Owned = RepoPathBuf;
542
543 fn to_owned(&self) -> Self::Owned {
544 let value = self.value.to_owned();
545 RepoPathBuf { value }
546 }
547
548 fn clone_into(&self, target: &mut Self::Owned) {
549 self.value.clone_into(&mut target.value);
550 }
551}
552
553impl Ord for RepoPath {
554 fn cmp(&self, other: &Self) -> Ordering {
555 debug_assert!(is_valid_repo_path_str(&self.value));
558 self.components().cmp(other.components())
559 }
560}
561
562impl Ord for RepoPathBuf {
563 fn cmp(&self, other: &Self) -> Ordering {
564 <RepoPath as Ord>::cmp(self, other)
565 }
566}
567
568impl PartialOrd for RepoPath {
569 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
570 Some(self.cmp(other))
571 }
572}
573
574impl PartialOrd for RepoPathBuf {
575 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
576 Some(self.cmp(other))
577 }
578}
579
580impl<P: AsRef<RepoPathComponent>> Extend<P> for RepoPathBuf {
581 fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
582 for component in iter {
583 if !self.value.is_empty() {
584 self.value.push('/');
585 }
586 self.value.push_str(component.as_ref().as_internal_str());
587 }
588 }
589}
590
591#[derive(Clone, Debug, Eq, Error, PartialEq)]
593#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())]
594pub struct InvalidRepoPathError {
595 pub path: RepoPathBuf,
597 pub source: InvalidRepoPathComponentError,
599}
600
601#[derive(Clone, Debug, Eq, Error, PartialEq)]
603#[error(r#"Invalid path component "{component}""#)]
604pub struct InvalidRepoPathComponentError {
605 pub component: Box<str>,
606}
607
608impl InvalidRepoPathComponentError {
609 pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError {
611 InvalidRepoPathError {
612 path: path.to_owned(),
613 source: self,
614 }
615 }
616}
617
618#[derive(Clone, Debug, Eq, Error, PartialEq)]
619pub enum RelativePathParseError {
620 #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
621 InvalidComponent {
622 component: Box<str>,
623 path: Box<Path>,
624 },
625 #[error(r#"Not valid UTF-8 path "{path}""#)]
626 InvalidUtf8 { path: Box<Path> },
627}
628
629#[derive(Clone, Debug, Eq, Error, PartialEq)]
630#[error(r#"Path "{input}" is not in the repo "{base}""#)]
631pub struct FsPathParseError {
632 pub base: Box<Path>,
634 pub input: Box<Path>,
636 pub source: RelativePathParseError,
638}
639
640fn is_valid_repo_path_component_str(value: &str) -> bool {
641 !value.is_empty() && !value.contains('/')
642}
643
644fn is_valid_repo_path_str(value: &str) -> bool {
645 !value.starts_with('/') && !value.ends_with('/') && !value.contains("//")
646}
647
648#[derive(Debug, Error)]
650pub enum UiPathParseError {
651 #[error(transparent)]
652 Fs(FsPathParseError),
653}
654
655#[derive(Debug, Clone)]
658pub enum RepoPathUiConverter {
659 Fs { cwd: PathBuf, base: PathBuf },
665 }
668
669impl RepoPathUiConverter {
670 pub fn format_file_path(&self, file: &RepoPath) -> String {
672 match self {
673 Self::Fs { cwd, base } => {
674 file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
675 .display()
676 .to_string()
677 }
678 }
679 }
680
681 pub fn format_copied_path(&self, source: &RepoPath, target: &RepoPath) -> String {
687 if source == target {
688 return self.format_file_path(source);
689 }
690 let mut formatted = String::new();
691 match self {
692 Self::Fs { cwd, base } => {
693 let source_path = file_util::relative_path(cwd, &source.to_fs_path_unchecked(base));
694 let target_path = file_util::relative_path(cwd, &target.to_fs_path_unchecked(base));
695
696 let source_components = source_path.components().collect_vec();
697 let target_components = target_path.components().collect_vec();
698
699 let prefix_count = source_components
700 .iter()
701 .zip(target_components.iter())
702 .take_while(|(source_component, target_component)| {
703 source_component == target_component
704 })
705 .count()
706 .min(source_components.len().saturating_sub(1))
707 .min(target_components.len().saturating_sub(1));
708
709 let suffix_count = source_components
710 .iter()
711 .skip(prefix_count)
712 .rev()
713 .zip(target_components.iter().skip(prefix_count).rev())
714 .take_while(|(source_component, target_component)| {
715 source_component == target_component
716 })
717 .count()
718 .min(source_components.len().saturating_sub(1))
719 .min(target_components.len().saturating_sub(1));
720
721 fn format_components(c: &[std::path::Component]) -> String {
722 c.iter().collect::<PathBuf>().display().to_string()
723 }
724
725 if prefix_count > 0 {
726 formatted.push_str(&format_components(&source_components[0..prefix_count]));
727 formatted.push_str(std::path::MAIN_SEPARATOR_STR);
728 }
729 formatted.push('{');
730 formatted.push_str(&format_components(
731 &source_components
732 [prefix_count..(source_components.len() - suffix_count).max(prefix_count)],
733 ));
734 formatted.push_str(" => ");
735 formatted.push_str(&format_components(
736 &target_components
737 [prefix_count..(target_components.len() - suffix_count).max(prefix_count)],
738 ));
739 formatted.push('}');
740 if suffix_count > 0 {
741 formatted.push_str(std::path::MAIN_SEPARATOR_STR);
742 formatted.push_str(&format_components(
743 &source_components[source_components.len() - suffix_count..],
744 ));
745 }
746 }
747 }
748 formatted
749 }
750
751 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
756 match self {
757 Self::Fs { cwd, base } => {
758 RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
759 }
760 }
761 }
762}
763
764#[cfg(test)]
765mod tests {
766 use std::panic;
767
768 use assert_matches::assert_matches;
769 use itertools::Itertools as _;
770
771 use super::*;
772 use crate::tests::new_temp_dir;
773
774 fn repo_path(value: &str) -> &RepoPath {
775 RepoPath::from_internal_string(value).unwrap()
776 }
777
778 fn repo_path_component(value: &str) -> &RepoPathComponent {
779 RepoPathComponent::new(value).unwrap()
780 }
781
782 #[test]
783 fn test_is_root() {
784 assert!(RepoPath::root().is_root());
785 assert!(repo_path("").is_root());
786 assert!(!repo_path("foo").is_root());
787 }
788
789 #[test]
790 fn test_from_internal_string() {
791 let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
792 assert_eq!(repo_path_buf(""), RepoPathBuf::root());
793 assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
794 assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
795 assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
796 assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
797
798 assert_eq!(repo_path(""), RepoPath::root());
799 assert!(panic::catch_unwind(|| repo_path("/")).is_err());
800 assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
801 assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
802 assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
803 }
804
805 #[test]
806 fn test_as_internal_file_string() {
807 assert_eq!(RepoPath::root().as_internal_file_string(), "");
808 assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
809 assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
810 }
811
812 #[test]
813 fn test_to_internal_dir_string() {
814 assert_eq!(RepoPath::root().to_internal_dir_string(), "");
815 assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
816 assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
817 }
818
819 #[test]
820 fn test_starts_with() {
821 assert!(repo_path("").starts_with(repo_path("")));
822 assert!(repo_path("x").starts_with(repo_path("")));
823 assert!(!repo_path("").starts_with(repo_path("x")));
824
825 assert!(repo_path("x").starts_with(repo_path("x")));
826 assert!(repo_path("x/y").starts_with(repo_path("x")));
827 assert!(!repo_path("xy").starts_with(repo_path("x")));
828 assert!(!repo_path("x/y").starts_with(repo_path("y")));
829
830 assert!(repo_path("x/y").starts_with(repo_path("x/y")));
831 assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
832 assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
833 assert!(!repo_path("x").starts_with(repo_path("x/y")));
834 assert!(!repo_path("xy").starts_with(repo_path("x/y")));
835 }
836
837 #[test]
838 fn test_strip_prefix() {
839 assert_eq!(
840 repo_path("").strip_prefix(repo_path("")),
841 Some(repo_path(""))
842 );
843 assert_eq!(
844 repo_path("x").strip_prefix(repo_path("")),
845 Some(repo_path("x"))
846 );
847 assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
848
849 assert_eq!(
850 repo_path("x").strip_prefix(repo_path("x")),
851 Some(repo_path(""))
852 );
853 assert_eq!(
854 repo_path("x/y").strip_prefix(repo_path("x")),
855 Some(repo_path("y"))
856 );
857 assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
858 assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
859
860 assert_eq!(
861 repo_path("x/y").strip_prefix(repo_path("x/y")),
862 Some(repo_path(""))
863 );
864 assert_eq!(
865 repo_path("x/y/z").strip_prefix(repo_path("x/y")),
866 Some(repo_path("z"))
867 );
868 assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
869 assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
870 assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
871 }
872
873 #[test]
874 fn test_order() {
875 assert!(RepoPath::root() < repo_path("dir"));
876 assert!(repo_path("dir") < repo_path("dirx"));
877 assert!(repo_path("dir") < repo_path("dir#"));
879 assert!(repo_path("dir") < repo_path("dir/sub"));
880 assert!(repo_path("dir/sub") < repo_path("dir#"));
881
882 assert!(repo_path("abc") < repo_path("dir/file"));
883 assert!(repo_path("dir") < repo_path("dir/file"));
884 assert!(repo_path("dis") > repo_path("dir/file"));
885 assert!(repo_path("xyz") > repo_path("dir/file"));
886 assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
887 }
888
889 #[test]
890 fn test_join() {
891 let root = RepoPath::root();
892 let dir = root.join(repo_path_component("dir"));
893 assert_eq!(dir.as_ref(), repo_path("dir"));
894 let subdir = dir.join(repo_path_component("subdir"));
895 assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
896 assert_eq!(
897 subdir.join(repo_path_component("file")).as_ref(),
898 repo_path("dir/subdir/file")
899 );
900 }
901
902 #[test]
903 fn test_extend() {
904 let mut path = RepoPathBuf::root();
905 path.extend(std::iter::empty::<RepoPathComponentBuf>());
906 assert_eq!(path.as_ref(), RepoPath::root());
907 path.extend([repo_path_component("dir")]);
908 assert_eq!(path.as_ref(), repo_path("dir"));
909 path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
910 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
911 path.extend(std::iter::empty::<RepoPathComponentBuf>());
912 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
913 }
914
915 #[test]
916 fn test_parent() {
917 let root = RepoPath::root();
918 let dir_component = repo_path_component("dir");
919 let subdir_component = repo_path_component("subdir");
920
921 let dir = root.join(dir_component);
922 let subdir = dir.join(subdir_component);
923
924 assert_eq!(root.parent(), None);
925 assert_eq!(dir.parent(), Some(root));
926 assert_eq!(subdir.parent(), Some(dir.as_ref()));
927 }
928
929 #[test]
930 fn test_split() {
931 let root = RepoPath::root();
932 let dir_component = repo_path_component("dir");
933 let file_component = repo_path_component("file");
934
935 let dir = root.join(dir_component);
936 let file = dir.join(file_component);
937
938 assert_eq!(root.split(), None);
939 assert_eq!(dir.split(), Some((root, dir_component)));
940 assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
941 }
942
943 #[test]
944 fn test_components() {
945 assert!(RepoPath::root().components().next().is_none());
946 assert_eq!(
947 repo_path("dir").components().collect_vec(),
948 vec![repo_path_component("dir")]
949 );
950 assert_eq!(
951 repo_path("dir/subdir").components().collect_vec(),
952 vec![repo_path_component("dir"), repo_path_component("subdir")]
953 );
954
955 assert!(RepoPath::root().components().next_back().is_none());
957 assert_eq!(
958 repo_path("dir").components().rev().collect_vec(),
959 vec![repo_path_component("dir")]
960 );
961 assert_eq!(
962 repo_path("dir/subdir").components().rev().collect_vec(),
963 vec![repo_path_component("subdir"), repo_path_component("dir")]
964 );
965 }
966
967 #[test]
968 fn test_ancestors() {
969 assert_eq!(
970 RepoPath::root().ancestors().collect_vec(),
971 vec![RepoPath::root()]
972 );
973 assert_eq!(
974 repo_path("dir").ancestors().collect_vec(),
975 vec![repo_path("dir"), RepoPath::root()]
976 );
977 assert_eq!(
978 repo_path("dir/subdir").ancestors().collect_vec(),
979 vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
980 );
981 }
982
983 #[test]
984 fn test_to_fs_path() {
985 assert_eq!(
986 repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
987 Path::new("base/dir")
988 );
989 assert_eq!(
990 repo_path("").to_fs_path(Path::new("")).unwrap(),
991 Path::new(".")
992 );
993 assert_eq!(
994 repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
995 Path::new("base/dir/file")
996 );
997 assert_eq!(
998 repo_path("some/deep/dir/file")
999 .to_fs_path(Path::new("base/dir"))
1000 .unwrap(),
1001 Path::new("base/dir/some/deep/dir/file")
1002 );
1003 assert_eq!(
1004 repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
1005 Path::new("dir/file")
1006 );
1007
1008 assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
1010 assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
1011 assert!(
1012 repo_path("dir/../file")
1013 .to_fs_path(Path::new("base"))
1014 .is_err()
1015 );
1016 assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
1017 assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
1018 assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
1019 assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
1020
1021 assert!(
1023 RepoPath::from_internal_string_unchecked("/")
1024 .to_fs_path(Path::new("base"))
1025 .is_err()
1026 );
1027 assert_eq!(
1028 RepoPath::from_internal_string_unchecked("a/")
1031 .to_fs_path(Path::new("base"))
1032 .unwrap(),
1033 Path::new("base/a")
1034 );
1035 assert!(
1036 RepoPath::from_internal_string_unchecked("/b")
1037 .to_fs_path(Path::new("base"))
1038 .is_err()
1039 );
1040 assert!(
1041 RepoPath::from_internal_string_unchecked("a//b")
1042 .to_fs_path(Path::new("base"))
1043 .is_err()
1044 );
1045
1046 assert!(
1048 RepoPathComponent::new_unchecked("wind/ows")
1049 .to_fs_name()
1050 .is_err()
1051 );
1052 assert!(
1053 RepoPathComponent::new_unchecked("./file")
1054 .to_fs_name()
1055 .is_err()
1056 );
1057 assert!(
1058 RepoPathComponent::new_unchecked("file/.")
1059 .to_fs_name()
1060 .is_err()
1061 );
1062 assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
1063
1064 if cfg!(windows) {
1066 assert!(
1067 repo_path(r#"wind\ows"#)
1068 .to_fs_path(Path::new("base"))
1069 .is_err()
1070 );
1071 assert!(
1072 repo_path(r#".\file"#)
1073 .to_fs_path(Path::new("base"))
1074 .is_err()
1075 );
1076 assert!(
1077 repo_path(r#"file\."#)
1078 .to_fs_path(Path::new("base"))
1079 .is_err()
1080 );
1081 assert!(
1082 repo_path(r#"c:/foo"#)
1083 .to_fs_path(Path::new("base"))
1084 .is_err()
1085 );
1086 }
1087 }
1088
1089 #[test]
1090 fn test_to_fs_path_unchecked() {
1091 assert_eq!(
1092 repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1093 Path::new("base/dir")
1094 );
1095 assert_eq!(
1096 repo_path("").to_fs_path_unchecked(Path::new("")),
1097 Path::new(".")
1098 );
1099 assert_eq!(
1100 repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1101 Path::new("base/dir/file")
1102 );
1103 assert_eq!(
1104 repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1105 Path::new("base/dir/some/deep/dir/file")
1106 );
1107 assert_eq!(
1108 repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1109 Path::new("dir/file")
1110 );
1111 }
1112
1113 #[test]
1114 fn parse_fs_path_wc_in_cwd() {
1115 let temp_dir = new_temp_dir();
1116 let cwd_path = temp_dir.path().join("repo");
1117 let wc_path = &cwd_path;
1118
1119 assert_eq!(
1120 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1121 Ok(RepoPath::root())
1122 );
1123 assert_eq!(
1124 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1125 Ok(RepoPath::root())
1126 );
1127 assert_eq!(
1128 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1129 Ok(repo_path("file"))
1130 );
1131 assert_eq!(
1133 RepoPathBuf::parse_fs_path(
1134 &cwd_path,
1135 wc_path,
1136 format!("dir{}file", std::path::MAIN_SEPARATOR)
1137 )
1138 .as_deref(),
1139 Ok(repo_path("dir/file"))
1140 );
1141 assert_eq!(
1142 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1143 Ok(repo_path("dir/file"))
1144 );
1145 assert_matches!(
1146 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1147 Err(FsPathParseError {
1148 source: RelativePathParseError::InvalidComponent { .. },
1149 ..
1150 })
1151 );
1152 assert_eq!(
1153 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1154 Ok(RepoPath::root())
1155 );
1156 assert_eq!(
1157 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1158 Ok(repo_path("file"))
1159 );
1160 assert_eq!(
1162 RepoPathBuf::parse_fs_path(
1163 &cwd_path,
1164 &cwd_path,
1165 cwd_path.join("../repo").to_str().unwrap()
1166 )
1167 .as_deref(),
1168 Ok(RepoPath::root())
1169 );
1170 }
1171
1172 #[test]
1173 fn parse_fs_path_wc_in_cwd_parent() {
1174 let temp_dir = new_temp_dir();
1175 let cwd_path = temp_dir.path().join("dir");
1176 let wc_path = cwd_path.parent().unwrap().to_path_buf();
1177
1178 assert_eq!(
1179 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1180 Ok(repo_path("dir"))
1181 );
1182 assert_eq!(
1183 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1184 Ok(repo_path("dir"))
1185 );
1186 assert_eq!(
1187 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1188 Ok(repo_path("dir/file"))
1189 );
1190 assert_eq!(
1191 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1192 Ok(repo_path("dir/subdir/file"))
1193 );
1194 assert_eq!(
1195 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1196 Ok(RepoPath::root())
1197 );
1198 assert_matches!(
1199 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1200 Err(FsPathParseError {
1201 source: RelativePathParseError::InvalidComponent { .. },
1202 ..
1203 })
1204 );
1205 assert_eq!(
1206 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1207 Ok(repo_path("other-dir/file"))
1208 );
1209 }
1210
1211 #[test]
1212 fn parse_fs_path_wc_in_cwd_child() {
1213 let temp_dir = new_temp_dir();
1214 let cwd_path = temp_dir.path().join("cwd");
1215 let wc_path = cwd_path.join("repo");
1216
1217 assert_matches!(
1218 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1219 Err(FsPathParseError {
1220 source: RelativePathParseError::InvalidComponent { .. },
1221 ..
1222 })
1223 );
1224 assert_matches!(
1225 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1226 Err(FsPathParseError {
1227 source: RelativePathParseError::InvalidComponent { .. },
1228 ..
1229 })
1230 );
1231 assert_eq!(
1232 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1233 Ok(RepoPath::root())
1234 );
1235 assert_eq!(
1236 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1237 Ok(repo_path("file"))
1238 );
1239 assert_eq!(
1240 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1241 Ok(repo_path("dir/file"))
1242 );
1243 }
1244
1245 #[test]
1246 fn test_format_copied_path() {
1247 let ui = RepoPathUiConverter::Fs {
1248 cwd: PathBuf::from("."),
1249 base: PathBuf::from("."),
1250 };
1251
1252 let format = |before, after| {
1253 ui.format_copied_path(repo_path(before), repo_path(after))
1254 .replace('\\', "/")
1255 };
1256
1257 assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1258 assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1259 assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1260 assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1261 assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1262 assert_eq!(
1263 format("one/two", "one/two/three/one/two"),
1264 "one/{ => two/three/one}/two"
1265 );
1266
1267 assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1268 assert_eq!(
1269 format("one/two/three", "one/four/three"),
1270 "one/{two => four}/three"
1271 );
1272 assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1273 assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1274 assert_eq!(format("two", "four"), "{two => four}");
1275 assert_eq!(format("file1", "file2"), "{file1 => file2}");
1276 assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1277 assert_eq!(
1278 format("x/something/something/2to1.txt", "x/something/2to1.txt"),
1279 "x/something/{something => }/2to1.txt"
1280 );
1281 assert_eq!(
1282 format("x/something/1to2.txt", "x/something/something/1to2.txt"),
1283 "x/something/{ => something}/1to2.txt"
1284 );
1285 }
1286
1287 #[test]
1288 fn test_split_common_prefix() {
1289 assert_eq!(
1290 repo_path("foo/bar").split_common_prefix(repo_path("foo/bar/baz")),
1291 (repo_path("foo/bar"), repo_path(""))
1292 );
1293
1294 assert_eq!(
1295 repo_path("foo/bar/baz").split_common_prefix(repo_path("foo/bar")),
1296 (repo_path("foo/bar"), repo_path("baz"))
1297 );
1298
1299 assert_eq!(
1300 repo_path("foo/bar/bing").split_common_prefix(repo_path("foo/bar/baz")),
1301 (repo_path("foo/bar"), repo_path("bing"))
1302 );
1303
1304 assert_eq!(
1305 repo_path("no/common/prefix").split_common_prefix(repo_path("foo/bar/baz")),
1306 (RepoPath::root(), repo_path("no/common/prefix"))
1307 );
1308
1309 assert_eq!(
1310 repo_path("same/path").split_common_prefix(repo_path("same/path")),
1311 (repo_path("same/path"), RepoPath::root())
1312 );
1313
1314 assert_eq!(
1315 RepoPath::root().split_common_prefix(repo_path("foo")),
1316 (RepoPath::root(), RepoPath::root())
1317 );
1318
1319 assert_eq!(
1320 RepoPath::root().split_common_prefix(RepoPath::root()),
1321 (RepoPath::root(), RepoPath::root())
1322 );
1323
1324 assert_eq!(
1325 repo_path("foo/bar").split_common_prefix(RepoPath::root()),
1326 (RepoPath::root(), repo_path("foo/bar"))
1327 );
1328 }
1329}