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