1#![expect(missing_docs)]
16
17use std::borrow::Borrow;
18use std::cmp::Ordering;
19use std::collections::HashMap;
20use std::fmt;
21use std::fmt::Debug;
22use std::fmt::Formatter;
23use std::iter;
24use std::iter::FusedIterator;
25use std::ops::Deref;
26use std::path::Component;
27use std::path::Path;
28use std::path::PathBuf;
29
30use itertools::Itertools as _;
31use ref_cast::RefCastCustom;
32use ref_cast::ref_cast_custom;
33use thiserror::Error;
34
35use crate::content_hash::ContentHash;
36use crate::file_util;
37use crate::merge::Diff;
38
39#[derive(ContentHash, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
41pub struct RepoPathComponentBuf {
42 value: String,
45}
46
47impl RepoPathComponentBuf {
48 pub fn new(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
53 let value: String = value.into();
54 if is_valid_repo_path_component_str(&value) {
55 Ok(Self { value })
56 } else {
57 Err(InvalidNewRepoPathError { value })
58 }
59 }
60}
61
62#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, RefCastCustom)]
64#[repr(transparent)]
65pub struct RepoPathComponent {
66 value: str,
67}
68
69impl RepoPathComponent {
70 pub fn new(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
75 if is_valid_repo_path_component_str(value) {
76 Ok(Self::new_unchecked(value))
77 } else {
78 Err(InvalidNewRepoPathError {
79 value: value.to_string(),
80 })
81 }
82 }
83
84 #[ref_cast_custom]
85 const fn new_unchecked(value: &str) -> &Self;
86
87 pub fn as_internal_str(&self) -> &str {
89 &self.value
90 }
91
92 pub fn to_fs_name(&self) -> Result<&str, InvalidRepoPathComponentError> {
95 let mut components = Path::new(&self.value).components().fuse();
96 match (components.next(), components.next()) {
97 (Some(Component::Normal(name)), None) if name == &self.value => Ok(&self.value),
100 _ => Err(InvalidRepoPathComponentError {
102 component: self.value.into(),
103 }),
104 }
105 }
106}
107
108impl Debug for RepoPathComponent {
109 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
110 write!(f, "{:?}", &self.value)
111 }
112}
113
114impl Debug for RepoPathComponentBuf {
115 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
116 <RepoPathComponent as Debug>::fmt(self, f)
117 }
118}
119
120impl AsRef<Self> for RepoPathComponent {
121 fn as_ref(&self) -> &Self {
122 self
123 }
124}
125
126impl AsRef<RepoPathComponent> for RepoPathComponentBuf {
127 fn as_ref(&self) -> &RepoPathComponent {
128 self
129 }
130}
131
132impl Borrow<RepoPathComponent> for RepoPathComponentBuf {
133 fn borrow(&self) -> &RepoPathComponent {
134 self
135 }
136}
137
138impl Deref for RepoPathComponentBuf {
139 type Target = RepoPathComponent;
140
141 fn deref(&self) -> &Self::Target {
142 RepoPathComponent::new_unchecked(&self.value)
143 }
144}
145
146impl ToOwned for RepoPathComponent {
147 type Owned = RepoPathComponentBuf;
148
149 fn to_owned(&self) -> Self::Owned {
150 let value = self.value.to_owned();
151 RepoPathComponentBuf { value }
152 }
153
154 fn clone_into(&self, target: &mut Self::Owned) {
155 self.value.clone_into(&mut target.value);
156 }
157}
158
159#[derive(Clone, Debug)]
161pub struct RepoPathComponentsIter<'a> {
162 value: &'a str,
163}
164
165impl<'a> RepoPathComponentsIter<'a> {
166 pub fn as_path(&self) -> &'a RepoPath {
168 RepoPath::from_internal_string_unchecked(self.value)
169 }
170}
171
172impl<'a> Iterator for RepoPathComponentsIter<'a> {
173 type Item = &'a RepoPathComponent;
174
175 fn next(&mut self) -> Option<Self::Item> {
176 if self.value.is_empty() {
177 return None;
178 }
179 let (name, remainder) = self
180 .value
181 .split_once('/')
182 .unwrap_or_else(|| (self.value, &self.value[self.value.len()..]));
183 self.value = remainder;
184 Some(RepoPathComponent::new_unchecked(name))
185 }
186}
187
188impl DoubleEndedIterator for RepoPathComponentsIter<'_> {
189 fn next_back(&mut self) -> Option<Self::Item> {
190 if self.value.is_empty() {
191 return None;
192 }
193 let (remainder, name) = self
194 .value
195 .rsplit_once('/')
196 .unwrap_or_else(|| (&self.value[..0], self.value));
197 self.value = remainder;
198 Some(RepoPathComponent::new_unchecked(name))
199 }
200}
201
202impl FusedIterator for RepoPathComponentsIter<'_> {}
203
204#[derive(ContentHash, Clone, Eq, Hash, PartialEq, serde::Serialize)]
206#[serde(transparent)]
207pub struct RepoPathBuf {
208 value: String,
211}
212
213#[derive(ContentHash, Eq, Hash, PartialEq, RefCastCustom, serde::Serialize)]
215#[repr(transparent)]
216#[serde(transparent)]
217pub struct RepoPath {
218 value: str,
219}
220
221impl Debug for RepoPath {
222 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
223 write!(f, "{:?}", &self.value)
224 }
225}
226
227impl Debug for RepoPathBuf {
228 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
229 <RepoPath as Debug>::fmt(self, f)
230 }
231}
232
233#[derive(Clone, Debug, Eq, Error, PartialEq)]
237#[error(r#"Invalid repo path input "{value}""#)]
238pub struct InvalidNewRepoPathError {
239 value: String,
240}
241
242impl RepoPathBuf {
243 pub const fn root() -> Self {
245 Self {
246 value: String::new(),
247 }
248 }
249
250 pub fn from_internal_string(value: impl Into<String>) -> Result<Self, InvalidNewRepoPathError> {
252 let value: String = value.into();
253 if is_valid_repo_path_str(&value) {
254 Ok(Self { value })
255 } else {
256 Err(InvalidNewRepoPathError { value })
257 }
258 }
259
260 pub fn from_relative_path(
264 relative_path: impl AsRef<Path>,
265 ) -> Result<Self, RelativePathParseError> {
266 let relative_path = relative_path.as_ref();
267 if relative_path == Path::new(".") {
268 return Ok(Self::root());
269 }
270
271 let mut components = relative_path
272 .components()
273 .map(|c| match c {
274 Component::Normal(name) => {
275 name.to_str()
276 .ok_or_else(|| RelativePathParseError::InvalidUtf8 {
277 path: relative_path.into(),
278 })
279 }
280 _ => Err(RelativePathParseError::InvalidComponent {
281 component: c.as_os_str().to_string_lossy().into(),
282 path: relative_path.into(),
283 }),
284 })
285 .fuse();
286 let mut value = String::with_capacity(relative_path.as_os_str().len());
287 if let Some(name) = components.next() {
288 value.push_str(name?);
289 }
290 for name in components {
291 value.push('/');
292 value.push_str(name?);
293 }
294 Ok(Self { value })
295 }
296
297 pub fn parse_fs_path(
303 cwd: &Path,
304 base: &Path,
305 input: impl AsRef<Path>,
306 ) -> Result<Self, FsPathParseError> {
307 let input = input.as_ref();
308 let abs_input_path = file_util::normalize_path(&cwd.join(input));
309 let repo_relative_path = file_util::relative_path(base, &abs_input_path);
310 Self::from_relative_path(repo_relative_path).map_err(|source| FsPathParseError {
311 base: file_util::relative_path(cwd, base).into(),
312 input: input.into(),
313 source,
314 })
315 }
316
317 pub fn into_internal_string(self) -> String {
319 self.value
320 }
321}
322
323impl RepoPath {
324 pub const fn root() -> &'static Self {
326 Self::from_internal_string_unchecked("")
327 }
328
329 pub fn from_internal_string(value: &str) -> Result<&Self, InvalidNewRepoPathError> {
334 if is_valid_repo_path_str(value) {
335 Ok(Self::from_internal_string_unchecked(value))
336 } else {
337 Err(InvalidNewRepoPathError {
338 value: value.to_owned(),
339 })
340 }
341 }
342
343 #[ref_cast_custom]
344 const fn from_internal_string_unchecked(value: &str) -> &Self;
345
346 pub fn to_internal_dir_string(&self) -> String {
351 if self.value.is_empty() {
352 String::new()
353 } else {
354 [&self.value, "/"].concat()
355 }
356 }
357
358 pub fn as_internal_file_string(&self) -> &str {
361 &self.value
362 }
363
364 pub fn to_fs_path(&self, base: &Path) -> Result<PathBuf, InvalidRepoPathError> {
369 let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
370 result.push(base);
371 for c in self.components() {
372 result.push(c.to_fs_name().map_err(|err| err.with_path(self))?);
373 }
374 if result.as_os_str().is_empty() {
375 result.push(".");
376 }
377 Ok(result)
378 }
379
380 pub fn to_fs_path_unchecked(&self, base: &Path) -> PathBuf {
386 let mut result = PathBuf::with_capacity(base.as_os_str().len() + self.value.len() + 1);
387 result.push(base);
388 result.extend(self.components().map(RepoPathComponent::as_internal_str));
389 if result.as_os_str().is_empty() {
390 result.push(".");
391 }
392 result
393 }
394
395 pub fn is_root(&self) -> bool {
396 self.value.is_empty()
397 }
398
399 pub fn starts_with(&self, base: &Self) -> bool {
401 self.strip_prefix(base).is_some()
402 }
403
404 pub fn strip_prefix(&self, base: &Self) -> Option<&Self> {
406 if base.value.is_empty() {
407 Some(self)
408 } else {
409 let tail = self.value.strip_prefix(&base.value)?;
410 if tail.is_empty() {
411 Some(Self::from_internal_string_unchecked(tail))
412 } else {
413 tail.strip_prefix('/')
414 .map(Self::from_internal_string_unchecked)
415 }
416 }
417 }
418
419 pub fn parent(&self) -> Option<&Self> {
421 self.split().map(|(parent, _)| parent)
422 }
423
424 pub fn split(&self) -> Option<(&Self, &RepoPathComponent)> {
426 let mut components = self.components();
427 let basename = components.next_back()?;
428 Some((components.as_path(), basename))
429 }
430
431 pub fn components(&self) -> RepoPathComponentsIter<'_> {
432 RepoPathComponentsIter { value: &self.value }
433 }
434
435 pub fn ancestors(&self) -> impl Iterator<Item = &Self> {
436 std::iter::successors(Some(self), |path| path.parent())
437 }
438
439 pub fn join(&self, entry: &RepoPathComponent) -> RepoPathBuf {
440 let value = if self.value.is_empty() {
441 entry.as_internal_str().to_owned()
442 } else {
443 [&self.value, "/", entry.as_internal_str()].concat()
444 };
445 RepoPathBuf { value }
446 }
447
448 pub fn split_common_prefix(&self, other: &Self) -> (&Self, &Self) {
482 let mut prefix_len = 0;
484
485 let common_components = self
486 .components()
487 .zip(other.components())
488 .take_while(|(prev_comp, this_comp)| prev_comp == this_comp);
489
490 for (self_comp, _other_comp) in common_components {
491 if prefix_len > 0 {
492 prefix_len += 1;
496 }
497
498 prefix_len += self_comp.value.len();
499 }
500
501 if prefix_len == 0 {
502 return (Self::root(), self);
504 }
505
506 if prefix_len == self.value.len() {
507 return (self, Self::root());
508 }
509
510 let common_prefix = Self::from_internal_string_unchecked(&self.value[..prefix_len]);
511 let remainder = Self::from_internal_string_unchecked(&self.value[prefix_len + 1..]);
512
513 (common_prefix, remainder)
514 }
515}
516
517impl AsRef<Self> for RepoPath {
518 fn as_ref(&self) -> &Self {
519 self
520 }
521}
522
523impl AsRef<RepoPath> for RepoPathBuf {
524 fn as_ref(&self) -> &RepoPath {
525 self
526 }
527}
528
529impl Borrow<RepoPath> for RepoPathBuf {
530 fn borrow(&self) -> &RepoPath {
531 self
532 }
533}
534
535impl Deref for RepoPathBuf {
536 type Target = RepoPath;
537
538 fn deref(&self) -> &Self::Target {
539 RepoPath::from_internal_string_unchecked(&self.value)
540 }
541}
542
543impl ToOwned for RepoPath {
544 type Owned = RepoPathBuf;
545
546 fn to_owned(&self) -> Self::Owned {
547 let value = self.value.to_owned();
548 RepoPathBuf { value }
549 }
550
551 fn clone_into(&self, target: &mut Self::Owned) {
552 self.value.clone_into(&mut target.value);
553 }
554}
555
556impl Ord for RepoPath {
557 fn cmp(&self, other: &Self) -> Ordering {
558 debug_assert!(is_valid_repo_path_str(&self.value));
561 self.components().cmp(other.components())
562 }
563}
564
565impl Ord for RepoPathBuf {
566 fn cmp(&self, other: &Self) -> Ordering {
567 <RepoPath as Ord>::cmp(self, other)
568 }
569}
570
571impl PartialOrd for RepoPath {
572 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
573 Some(self.cmp(other))
574 }
575}
576
577impl PartialOrd for RepoPathBuf {
578 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
579 Some(self.cmp(other))
580 }
581}
582
583impl<P: AsRef<RepoPathComponent>> Extend<P> for RepoPathBuf {
584 fn extend<T: IntoIterator<Item = P>>(&mut self, iter: T) {
585 for component in iter {
586 if !self.value.is_empty() {
587 self.value.push('/');
588 }
589 self.value.push_str(component.as_ref().as_internal_str());
590 }
591 }
592}
593
594#[derive(Clone, Debug, Eq, Error, PartialEq)]
596#[error(r#"Invalid repository path "{}""#, path.as_internal_file_string())]
597pub struct InvalidRepoPathError {
598 pub path: RepoPathBuf,
600 pub source: InvalidRepoPathComponentError,
602}
603
604#[derive(Clone, Debug, Eq, Error, PartialEq)]
606#[error(r#"Invalid path component "{component}""#)]
607pub struct InvalidRepoPathComponentError {
608 pub component: Box<str>,
609}
610
611impl InvalidRepoPathComponentError {
612 pub fn with_path(self, path: &RepoPath) -> InvalidRepoPathError {
614 InvalidRepoPathError {
615 path: path.to_owned(),
616 source: self,
617 }
618 }
619}
620
621#[derive(Clone, Debug, Eq, Error, PartialEq)]
622pub enum RelativePathParseError {
623 #[error(r#"Invalid component "{component}" in repo-relative path "{path}""#)]
624 InvalidComponent {
625 component: Box<str>,
626 path: Box<Path>,
627 },
628 #[error(r#"Not valid UTF-8 path "{path}""#)]
629 InvalidUtf8 { path: Box<Path> },
630}
631
632#[derive(Clone, Debug, Eq, Error, PartialEq)]
633#[error(r#"Path "{input}" is not in the repo "{base}""#)]
634pub struct FsPathParseError {
635 pub base: Box<Path>,
637 pub input: Box<Path>,
639 pub source: RelativePathParseError,
641}
642
643fn is_valid_repo_path_component_str(value: &str) -> bool {
644 !value.is_empty() && !value.contains('/')
645}
646
647fn is_valid_repo_path_str(value: &str) -> bool {
648 !value.starts_with('/') && !value.ends_with('/') && !value.contains("//")
649}
650
651#[derive(Debug, Error)]
653pub enum UiPathParseError {
654 #[error(transparent)]
655 Fs(FsPathParseError),
656}
657
658#[derive(Debug, Clone)]
661pub enum RepoPathUiConverter {
662 Fs { cwd: PathBuf, base: PathBuf },
668 }
671
672impl RepoPathUiConverter {
673 pub fn format_file_path(&self, file: &RepoPath) -> String {
675 match self {
676 Self::Fs { cwd, base } => {
677 file_util::relative_path(cwd, &file.to_fs_path_unchecked(base))
678 .display()
679 .to_string()
680 }
681 }
682 }
683
684 pub fn format_copied_path(&self, paths: Diff<&RepoPath>) -> String {
690 match self {
691 Self::Fs { .. } => {
692 let paths = paths.map(|path| self.format_file_path(path));
693 collapse_copied_path(paths.as_deref(), std::path::MAIN_SEPARATOR)
694 }
695 }
696 }
697
698 pub fn parse_file_path(&self, input: &str) -> Result<RepoPathBuf, UiPathParseError> {
703 match self {
704 Self::Fs { cwd, base } => {
705 RepoPathBuf::parse_fs_path(cwd, base, input).map_err(UiPathParseError::Fs)
706 }
707 }
708 }
709}
710
711fn collapse_copied_path(paths: Diff<&str>, separator: char) -> String {
712 let components = paths.map(|path| path.split_inclusive(separator));
715 let prefix_len: usize = iter::zip(components.before, components.after)
716 .take_while(|(before, after)| before == after)
717 .map(|(_, after)| after.len())
718 .sum();
719 if paths.before.len() == prefix_len && paths.after.len() == prefix_len {
720 return paths.after.to_owned();
721 }
722
723 let components = paths.map(|path| {
727 let mut remainder = &path[prefix_len.saturating_sub(1)..];
728 iter::from_fn(move || {
729 let pos = remainder.rfind(separator)?;
730 let (prefix, last) = remainder.split_at(pos);
731 remainder = prefix;
732 Some(last)
733 })
734 });
735 let suffix_len: usize = iter::zip(components.before, components.after)
736 .take_while(|(before, after)| before == after)
737 .map(|(_, after)| after.len())
738 .sum();
739
740 let middle = paths.map(|path| path.get(prefix_len..path.len() - suffix_len).unwrap_or(""));
744
745 let mut collapsed = String::new();
746 collapsed.push_str(&paths.after[..prefix_len]);
747 collapsed.push('{');
748 collapsed.push_str(middle.before);
749 collapsed.push_str(" => ");
750 collapsed.push_str(middle.after);
751 collapsed.push('}');
752 collapsed.push_str(&paths.after[paths.after.len() - suffix_len..]);
753 collapsed
754}
755
756#[derive(Clone, Default, Eq, PartialEq)]
758pub struct RepoPathTree<V> {
759 entries: HashMap<RepoPathComponentBuf, Self>,
760 value: V,
761}
762
763impl<V> RepoPathTree<V> {
764 pub fn value(&self) -> &V {
766 &self.value
767 }
768
769 pub fn value_mut(&mut self) -> &mut V {
771 &mut self.value
772 }
773
774 pub fn set_value(&mut self, value: V) {
776 self.value = value;
777 }
778
779 pub fn children(&self) -> impl Iterator<Item = (&RepoPathComponent, &Self)> {
781 self.entries
782 .iter()
783 .map(|(component, value)| (component.as_ref(), value))
784 }
785
786 pub fn has_children(&self) -> bool {
788 !self.entries.is_empty()
789 }
790
791 pub fn add(&mut self, path: &RepoPath) -> &mut Self
793 where
794 V: Default,
795 {
796 path.components().fold(self, |sub, name| {
797 if !sub.entries.contains_key(name) {
799 sub.entries.insert(name.to_owned(), Self::default());
800 }
801 sub.entries.get_mut(name).unwrap()
802 })
803 }
804
805 pub fn get(&self, path: &RepoPath) -> Option<&Self> {
808 path.components()
809 .try_fold(self, |sub, name| sub.entries.get(name))
810 }
811
812 pub fn walk_to<'a, 'b>(
815 &'a self,
816 path: &'b RepoPath,
817 ) -> impl Iterator<Item = (&'a Self, &'b RepoPath)> {
818 iter::successors(Some((self, path)), |(sub, path)| {
819 let mut components = path.components();
820 let name = components.next()?;
821 Some((sub.entries.get(name)?, components.as_path()))
822 })
823 }
824}
825
826impl<V: Debug> Debug for RepoPathTree<V> {
827 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
828 self.value.fmt(f)?;
829 f.write_str(" ")?;
830 f.debug_map()
831 .entries(
832 self.entries
833 .iter()
834 .sorted_unstable_by_key(|&(name, _)| name),
835 )
836 .finish()
837 }
838}
839
840#[cfg(test)]
841mod tests {
842 use std::panic;
843
844 use assert_matches::assert_matches;
845 use itertools::Itertools as _;
846
847 use super::*;
848 use crate::tests::new_temp_dir;
849
850 fn repo_path(value: &str) -> &RepoPath {
851 RepoPath::from_internal_string(value).unwrap()
852 }
853
854 fn repo_path_component(value: &str) -> &RepoPathComponent {
855 RepoPathComponent::new(value).unwrap()
856 }
857
858 #[test]
859 fn test_is_root() {
860 assert!(RepoPath::root().is_root());
861 assert!(repo_path("").is_root());
862 assert!(!repo_path("foo").is_root());
863 }
864
865 #[test]
866 fn test_from_internal_string() {
867 let repo_path_buf = |value: &str| RepoPathBuf::from_internal_string(value).unwrap();
868 assert_eq!(repo_path_buf(""), RepoPathBuf::root());
869 assert!(panic::catch_unwind(|| repo_path_buf("/")).is_err());
870 assert!(panic::catch_unwind(|| repo_path_buf("/x")).is_err());
871 assert!(panic::catch_unwind(|| repo_path_buf("x/")).is_err());
872 assert!(panic::catch_unwind(|| repo_path_buf("x//y")).is_err());
873
874 assert_eq!(repo_path(""), RepoPath::root());
875 assert!(panic::catch_unwind(|| repo_path("/")).is_err());
876 assert!(panic::catch_unwind(|| repo_path("/x")).is_err());
877 assert!(panic::catch_unwind(|| repo_path("x/")).is_err());
878 assert!(panic::catch_unwind(|| repo_path("x//y")).is_err());
879 }
880
881 #[test]
882 fn test_as_internal_file_string() {
883 assert_eq!(RepoPath::root().as_internal_file_string(), "");
884 assert_eq!(repo_path("dir").as_internal_file_string(), "dir");
885 assert_eq!(repo_path("dir/file").as_internal_file_string(), "dir/file");
886 }
887
888 #[test]
889 fn test_to_internal_dir_string() {
890 assert_eq!(RepoPath::root().to_internal_dir_string(), "");
891 assert_eq!(repo_path("dir").to_internal_dir_string(), "dir/");
892 assert_eq!(repo_path("dir/file").to_internal_dir_string(), "dir/file/");
893 }
894
895 #[test]
896 fn test_starts_with() {
897 assert!(repo_path("").starts_with(repo_path("")));
898 assert!(repo_path("x").starts_with(repo_path("")));
899 assert!(!repo_path("").starts_with(repo_path("x")));
900
901 assert!(repo_path("x").starts_with(repo_path("x")));
902 assert!(repo_path("x/y").starts_with(repo_path("x")));
903 assert!(!repo_path("xy").starts_with(repo_path("x")));
904 assert!(!repo_path("x/y").starts_with(repo_path("y")));
905
906 assert!(repo_path("x/y").starts_with(repo_path("x/y")));
907 assert!(repo_path("x/y/z").starts_with(repo_path("x/y")));
908 assert!(!repo_path("x/yz").starts_with(repo_path("x/y")));
909 assert!(!repo_path("x").starts_with(repo_path("x/y")));
910 assert!(!repo_path("xy").starts_with(repo_path("x/y")));
911 }
912
913 #[test]
914 fn test_strip_prefix() {
915 assert_eq!(
916 repo_path("").strip_prefix(repo_path("")),
917 Some(repo_path(""))
918 );
919 assert_eq!(
920 repo_path("x").strip_prefix(repo_path("")),
921 Some(repo_path("x"))
922 );
923 assert_eq!(repo_path("").strip_prefix(repo_path("x")), None);
924
925 assert_eq!(
926 repo_path("x").strip_prefix(repo_path("x")),
927 Some(repo_path(""))
928 );
929 assert_eq!(
930 repo_path("x/y").strip_prefix(repo_path("x")),
931 Some(repo_path("y"))
932 );
933 assert_eq!(repo_path("xy").strip_prefix(repo_path("x")), None);
934 assert_eq!(repo_path("x/y").strip_prefix(repo_path("y")), None);
935
936 assert_eq!(
937 repo_path("x/y").strip_prefix(repo_path("x/y")),
938 Some(repo_path(""))
939 );
940 assert_eq!(
941 repo_path("x/y/z").strip_prefix(repo_path("x/y")),
942 Some(repo_path("z"))
943 );
944 assert_eq!(repo_path("x/yz").strip_prefix(repo_path("x/y")), None);
945 assert_eq!(repo_path("x").strip_prefix(repo_path("x/y")), None);
946 assert_eq!(repo_path("xy").strip_prefix(repo_path("x/y")), None);
947 }
948
949 #[test]
950 fn test_order() {
951 assert!(RepoPath::root() < repo_path("dir"));
952 assert!(repo_path("dir") < repo_path("dirx"));
953 assert!(repo_path("dir") < repo_path("dir#"));
955 assert!(repo_path("dir") < repo_path("dir/sub"));
956 assert!(repo_path("dir/sub") < repo_path("dir#"));
957
958 assert!(repo_path("abc") < repo_path("dir/file"));
959 assert!(repo_path("dir") < repo_path("dir/file"));
960 assert!(repo_path("dis") > repo_path("dir/file"));
961 assert!(repo_path("xyz") > repo_path("dir/file"));
962 assert!(repo_path("dir1/xyz") < repo_path("dir2/abc"));
963 }
964
965 #[test]
966 fn test_join() {
967 let root = RepoPath::root();
968 let dir = root.join(repo_path_component("dir"));
969 assert_eq!(dir.as_ref(), repo_path("dir"));
970 let subdir = dir.join(repo_path_component("subdir"));
971 assert_eq!(subdir.as_ref(), repo_path("dir/subdir"));
972 assert_eq!(
973 subdir.join(repo_path_component("file")).as_ref(),
974 repo_path("dir/subdir/file")
975 );
976 }
977
978 #[test]
979 fn test_extend() {
980 let mut path = RepoPathBuf::root();
981 path.extend(std::iter::empty::<RepoPathComponentBuf>());
982 assert_eq!(path.as_ref(), RepoPath::root());
983 path.extend([repo_path_component("dir")]);
984 assert_eq!(path.as_ref(), repo_path("dir"));
985 path.extend(std::iter::repeat_n(repo_path_component("subdir"), 3));
986 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
987 path.extend(std::iter::empty::<RepoPathComponentBuf>());
988 assert_eq!(path.as_ref(), repo_path("dir/subdir/subdir/subdir"));
989 }
990
991 #[test]
992 fn test_parent() {
993 let root = RepoPath::root();
994 let dir_component = repo_path_component("dir");
995 let subdir_component = repo_path_component("subdir");
996
997 let dir = root.join(dir_component);
998 let subdir = dir.join(subdir_component);
999
1000 assert_eq!(root.parent(), None);
1001 assert_eq!(dir.parent(), Some(root));
1002 assert_eq!(subdir.parent(), Some(dir.as_ref()));
1003 }
1004
1005 #[test]
1006 fn test_split() {
1007 let root = RepoPath::root();
1008 let dir_component = repo_path_component("dir");
1009 let file_component = repo_path_component("file");
1010
1011 let dir = root.join(dir_component);
1012 let file = dir.join(file_component);
1013
1014 assert_eq!(root.split(), None);
1015 assert_eq!(dir.split(), Some((root, dir_component)));
1016 assert_eq!(file.split(), Some((dir.as_ref(), file_component)));
1017 }
1018
1019 #[test]
1020 fn test_components() {
1021 assert!(RepoPath::root().components().next().is_none());
1022 assert_eq!(
1023 repo_path("dir").components().collect_vec(),
1024 vec![repo_path_component("dir")]
1025 );
1026 assert_eq!(
1027 repo_path("dir/subdir").components().collect_vec(),
1028 vec![repo_path_component("dir"), repo_path_component("subdir")]
1029 );
1030
1031 assert!(RepoPath::root().components().next_back().is_none());
1033 assert_eq!(
1034 repo_path("dir").components().rev().collect_vec(),
1035 vec![repo_path_component("dir")]
1036 );
1037 assert_eq!(
1038 repo_path("dir/subdir").components().rev().collect_vec(),
1039 vec![repo_path_component("subdir"), repo_path_component("dir")]
1040 );
1041 }
1042
1043 #[test]
1044 fn test_ancestors() {
1045 assert_eq!(
1046 RepoPath::root().ancestors().collect_vec(),
1047 vec![RepoPath::root()]
1048 );
1049 assert_eq!(
1050 repo_path("dir").ancestors().collect_vec(),
1051 vec![repo_path("dir"), RepoPath::root()]
1052 );
1053 assert_eq!(
1054 repo_path("dir/subdir").ancestors().collect_vec(),
1055 vec![repo_path("dir/subdir"), repo_path("dir"), RepoPath::root()]
1056 );
1057 }
1058
1059 #[test]
1060 fn test_to_fs_path() {
1061 assert_eq!(
1062 repo_path("").to_fs_path(Path::new("base/dir")).unwrap(),
1063 Path::new("base/dir")
1064 );
1065 assert_eq!(
1066 repo_path("").to_fs_path(Path::new("")).unwrap(),
1067 Path::new(".")
1068 );
1069 assert_eq!(
1070 repo_path("file").to_fs_path(Path::new("base/dir")).unwrap(),
1071 Path::new("base/dir/file")
1072 );
1073 assert_eq!(
1074 repo_path("some/deep/dir/file")
1075 .to_fs_path(Path::new("base/dir"))
1076 .unwrap(),
1077 Path::new("base/dir/some/deep/dir/file")
1078 );
1079 assert_eq!(
1080 repo_path("dir/file").to_fs_path(Path::new("")).unwrap(),
1081 Path::new("dir/file")
1082 );
1083
1084 assert!(repo_path(".").to_fs_path(Path::new("base")).is_err());
1086 assert!(repo_path("..").to_fs_path(Path::new("base")).is_err());
1087 assert!(
1088 repo_path("dir/../file")
1089 .to_fs_path(Path::new("base"))
1090 .is_err()
1091 );
1092 assert!(repo_path("./file").to_fs_path(Path::new("base")).is_err());
1093 assert!(repo_path("file/.").to_fs_path(Path::new("base")).is_err());
1094 assert!(repo_path("../file").to_fs_path(Path::new("base")).is_err());
1095 assert!(repo_path("file/..").to_fs_path(Path::new("base")).is_err());
1096
1097 assert!(
1099 RepoPath::from_internal_string_unchecked("/")
1100 .to_fs_path(Path::new("base"))
1101 .is_err()
1102 );
1103 assert_eq!(
1104 RepoPath::from_internal_string_unchecked("a/")
1107 .to_fs_path(Path::new("base"))
1108 .unwrap(),
1109 Path::new("base/a")
1110 );
1111 assert!(
1112 RepoPath::from_internal_string_unchecked("/b")
1113 .to_fs_path(Path::new("base"))
1114 .is_err()
1115 );
1116 assert!(
1117 RepoPath::from_internal_string_unchecked("a//b")
1118 .to_fs_path(Path::new("base"))
1119 .is_err()
1120 );
1121
1122 assert!(
1124 RepoPathComponent::new_unchecked("wind/ows")
1125 .to_fs_name()
1126 .is_err()
1127 );
1128 assert!(
1129 RepoPathComponent::new_unchecked("./file")
1130 .to_fs_name()
1131 .is_err()
1132 );
1133 assert!(
1134 RepoPathComponent::new_unchecked("file/.")
1135 .to_fs_name()
1136 .is_err()
1137 );
1138 assert!(RepoPathComponent::new_unchecked("/").to_fs_name().is_err());
1139
1140 if cfg!(windows) {
1142 assert!(
1143 repo_path(r#"wind\ows"#)
1144 .to_fs_path(Path::new("base"))
1145 .is_err()
1146 );
1147 assert!(
1148 repo_path(r#".\file"#)
1149 .to_fs_path(Path::new("base"))
1150 .is_err()
1151 );
1152 assert!(
1153 repo_path(r#"file\."#)
1154 .to_fs_path(Path::new("base"))
1155 .is_err()
1156 );
1157 assert!(
1158 repo_path(r#"c:/foo"#)
1159 .to_fs_path(Path::new("base"))
1160 .is_err()
1161 );
1162 }
1163 }
1164
1165 #[test]
1166 fn test_to_fs_path_unchecked() {
1167 assert_eq!(
1168 repo_path("").to_fs_path_unchecked(Path::new("base/dir")),
1169 Path::new("base/dir")
1170 );
1171 assert_eq!(
1172 repo_path("").to_fs_path_unchecked(Path::new("")),
1173 Path::new(".")
1174 );
1175 assert_eq!(
1176 repo_path("file").to_fs_path_unchecked(Path::new("base/dir")),
1177 Path::new("base/dir/file")
1178 );
1179 assert_eq!(
1180 repo_path("some/deep/dir/file").to_fs_path_unchecked(Path::new("base/dir")),
1181 Path::new("base/dir/some/deep/dir/file")
1182 );
1183 assert_eq!(
1184 repo_path("dir/file").to_fs_path_unchecked(Path::new("")),
1185 Path::new("dir/file")
1186 );
1187 }
1188
1189 #[test]
1190 fn parse_fs_path_wc_in_cwd() {
1191 let temp_dir = new_temp_dir();
1192 let cwd_path = temp_dir.path().join("repo");
1193 let wc_path = &cwd_path;
1194
1195 assert_eq!(
1196 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "").as_deref(),
1197 Ok(RepoPath::root())
1198 );
1199 assert_eq!(
1200 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".").as_deref(),
1201 Ok(RepoPath::root())
1202 );
1203 assert_eq!(
1204 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "file").as_deref(),
1205 Ok(repo_path("file"))
1206 );
1207 assert_eq!(
1209 RepoPathBuf::parse_fs_path(
1210 &cwd_path,
1211 wc_path,
1212 format!("dir{}file", std::path::MAIN_SEPARATOR)
1213 )
1214 .as_deref(),
1215 Ok(repo_path("dir/file"))
1216 );
1217 assert_eq!(
1218 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, "dir/file").as_deref(),
1219 Ok(repo_path("dir/file"))
1220 );
1221 assert_matches!(
1222 RepoPathBuf::parse_fs_path(&cwd_path, wc_path, ".."),
1223 Err(FsPathParseError {
1224 source: RelativePathParseError::InvalidComponent { .. },
1225 ..
1226 })
1227 );
1228 assert_eq!(
1229 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo").as_deref(),
1230 Ok(RepoPath::root())
1231 );
1232 assert_eq!(
1233 RepoPathBuf::parse_fs_path(&cwd_path, &cwd_path, "../repo/file").as_deref(),
1234 Ok(repo_path("file"))
1235 );
1236 assert_eq!(
1238 RepoPathBuf::parse_fs_path(
1239 &cwd_path,
1240 &cwd_path,
1241 cwd_path.join("../repo").to_str().unwrap()
1242 )
1243 .as_deref(),
1244 Ok(RepoPath::root())
1245 );
1246 }
1247
1248 #[test]
1249 fn parse_fs_path_wc_in_cwd_parent() {
1250 let temp_dir = new_temp_dir();
1251 let cwd_path = temp_dir.path().join("dir");
1252 let wc_path = cwd_path.parent().unwrap().to_path_buf();
1253
1254 assert_eq!(
1255 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "").as_deref(),
1256 Ok(repo_path("dir"))
1257 );
1258 assert_eq!(
1259 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ".").as_deref(),
1260 Ok(repo_path("dir"))
1261 );
1262 assert_eq!(
1263 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "file").as_deref(),
1264 Ok(repo_path("dir/file"))
1265 );
1266 assert_eq!(
1267 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "subdir/file").as_deref(),
1268 Ok(repo_path("dir/subdir/file"))
1269 );
1270 assert_eq!(
1271 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "..").as_deref(),
1272 Ok(RepoPath::root())
1273 );
1274 assert_matches!(
1275 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../.."),
1276 Err(FsPathParseError {
1277 source: RelativePathParseError::InvalidComponent { .. },
1278 ..
1279 })
1280 );
1281 assert_eq!(
1282 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "../other-dir/file").as_deref(),
1283 Ok(repo_path("other-dir/file"))
1284 );
1285 }
1286
1287 #[test]
1288 fn parse_fs_path_wc_in_cwd_child() {
1289 let temp_dir = new_temp_dir();
1290 let cwd_path = temp_dir.path().join("cwd");
1291 let wc_path = cwd_path.join("repo");
1292
1293 assert_matches!(
1294 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, ""),
1295 Err(FsPathParseError {
1296 source: RelativePathParseError::InvalidComponent { .. },
1297 ..
1298 })
1299 );
1300 assert_matches!(
1301 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "not-repo"),
1302 Err(FsPathParseError {
1303 source: RelativePathParseError::InvalidComponent { .. },
1304 ..
1305 })
1306 );
1307 assert_eq!(
1308 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo").as_deref(),
1309 Ok(RepoPath::root())
1310 );
1311 assert_eq!(
1312 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/file").as_deref(),
1313 Ok(repo_path("file"))
1314 );
1315 assert_eq!(
1316 RepoPathBuf::parse_fs_path(&cwd_path, &wc_path, "repo/dir/file").as_deref(),
1317 Ok(repo_path("dir/file"))
1318 );
1319 }
1320
1321 #[test]
1322 fn test_format_copied_path() {
1323 let ui = RepoPathUiConverter::Fs {
1324 cwd: PathBuf::from("."),
1325 base: PathBuf::from("."),
1326 };
1327
1328 let format = |before, after| {
1329 ui.format_copied_path(Diff::new(repo_path(before), repo_path(after)))
1330 .replace('\\', "/")
1331 };
1332
1333 assert_eq!(format("one/two/three", "one/two/three"), "one/two/three");
1334 assert_eq!(format("one/two", "one/two/three"), "one/{two => two/three}");
1335 assert_eq!(format("one/two", "zero/one/two"), "{one => zero/one}/two");
1336 assert_eq!(format("one/two/three", "one/two"), "one/{two/three => two}");
1337 assert_eq!(format("zero/one/two", "one/two"), "{zero/one => one}/two");
1338 assert_eq!(
1339 format("one/two", "one/two/three/one/two"),
1340 "one/{ => two/three/one}/two"
1341 );
1342
1343 assert_eq!(format("two/three", "four/three"), "{two => four}/three");
1344 assert_eq!(
1345 format("one/two/three", "one/four/three"),
1346 "one/{two => four}/three"
1347 );
1348 assert_eq!(format("one/two/three", "one/three"), "one/{two => }/three");
1349 assert_eq!(format("one/two", "one/four"), "one/{two => four}");
1350 assert_eq!(format("two", "four"), "{two => four}");
1351 assert_eq!(format("file1", "file2"), "{file1 => file2}");
1352 assert_eq!(format("file-1", "file-2"), "{file-1 => file-2}");
1353 assert_eq!(
1354 format("x/something/something/2to1.txt", "x/something/2to1.txt"),
1355 "x/something/{something => }/2to1.txt"
1356 );
1357 assert_eq!(
1358 format("x/something/1to2.txt", "x/something/something/1to2.txt"),
1359 "x/something/{ => something}/1to2.txt"
1360 );
1361 }
1362
1363 #[test]
1364 fn test_split_common_prefix() {
1365 assert_eq!(
1366 repo_path("foo/bar").split_common_prefix(repo_path("foo/bar/baz")),
1367 (repo_path("foo/bar"), repo_path(""))
1368 );
1369
1370 assert_eq!(
1371 repo_path("foo/bar/baz").split_common_prefix(repo_path("foo/bar")),
1372 (repo_path("foo/bar"), repo_path("baz"))
1373 );
1374
1375 assert_eq!(
1376 repo_path("foo/bar/bing").split_common_prefix(repo_path("foo/bar/baz")),
1377 (repo_path("foo/bar"), repo_path("bing"))
1378 );
1379
1380 assert_eq!(
1381 repo_path("no/common/prefix").split_common_prefix(repo_path("foo/bar/baz")),
1382 (RepoPath::root(), repo_path("no/common/prefix"))
1383 );
1384
1385 assert_eq!(
1386 repo_path("same/path").split_common_prefix(repo_path("same/path")),
1387 (repo_path("same/path"), RepoPath::root())
1388 );
1389
1390 assert_eq!(
1391 RepoPath::root().split_common_prefix(repo_path("foo")),
1392 (RepoPath::root(), RepoPath::root())
1393 );
1394
1395 assert_eq!(
1396 RepoPath::root().split_common_prefix(RepoPath::root()),
1397 (RepoPath::root(), RepoPath::root())
1398 );
1399
1400 assert_eq!(
1401 repo_path("foo/bar").split_common_prefix(RepoPath::root()),
1402 (RepoPath::root(), repo_path("foo/bar"))
1403 );
1404 }
1405}