1use std::fmt::{Display, Formatter};
2use std::io;
3use std::path::Path;
4use std::str::FromStr;
5
6use thiserror::Error;
7use uv_cache_key::{CacheKey, CacheKeyHasher};
8use uv_distribution_filename::DistExtension;
9use uv_fs::{CWD, PortablePath, PortablePathBuf, relative_to};
10use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
11use uv_normalize::{ExtraName, GroupName, PackageName};
12use uv_pep440::VersionSpecifiers;
13use uv_pep508::{
14 MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, marker,
15};
16use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
17
18use crate::{IndexMetadata, IndexUrl};
19
20use uv_pypi_types::{
21 ConflictItem, Hashes, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitUrl, ParsedPathUrl,
22 ParsedUrl, ParsedUrlError, VerbatimParsedUrl,
23};
24
25#[derive(Debug, Error)]
26pub enum RequirementError {
27 #[error(transparent)]
28 VerbatimUrlError(#[from] uv_pep508::VerbatimUrlError),
29 #[error(transparent)]
30 ParsedUrlError(#[from] ParsedUrlError),
31 #[error(transparent)]
32 UrlParseError(#[from] DisplaySafeUrlError),
33 #[error(transparent)]
34 OidParseError(#[from] OidParseError),
35 #[error(transparent)]
36 GitUrlParse(#[from] GitUrlParseError),
37}
38
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
48pub struct Requirement {
49 pub name: PackageName,
50 #[serde(skip_serializing_if = "<[ExtraName]>::is_empty", default)]
51 pub extras: Box<[ExtraName]>,
52 #[serde(skip_serializing_if = "<[GroupName]>::is_empty", default)]
53 pub groups: Box<[GroupName]>,
54 #[serde(
55 skip_serializing_if = "marker::ser::is_empty",
56 serialize_with = "marker::ser::serialize",
57 default
58 )]
59 pub marker: MarkerTree,
60 #[serde(flatten)]
61 pub source: RequirementSource,
62 #[serde(skip)]
63 pub origin: Option<RequirementOrigin>,
64}
65
66impl Requirement {
67 pub fn evaluate_markers(&self, env: Option<&MarkerEnvironment>, extras: &[ExtraName]) -> bool {
73 self.marker.evaluate_optional_environment(env, extras)
74 }
75
76 pub fn is_editable(&self) -> bool {
78 self.source.is_editable()
79 }
80
81 pub fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
83 Ok(Self {
84 source: self.source.relative_to(path)?,
85 ..self
86 })
87 }
88
89 #[must_use]
91 pub fn to_absolute(self, path: &Path) -> Self {
92 Self {
93 source: self.source.to_absolute(path),
94 ..self
95 }
96 }
97
98 pub fn hashes(&self) -> Option<Hashes> {
100 let RequirementSource::Url { ref url, .. } = self.source else {
101 return None;
102 };
103 let fragment = url.fragment()?;
104 Hashes::parse_fragment(fragment).ok()
105 }
106
107 #[must_use]
109 pub fn with_origin(self, origin: RequirementOrigin) -> Self {
110 Self {
111 origin: Some(origin),
112 ..self
113 }
114 }
115}
116
117impl std::hash::Hash for Requirement {
118 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
119 let Self {
120 name,
121 extras,
122 groups,
123 marker,
124 source,
125 origin: _,
126 } = self;
127 name.hash(state);
128 extras.hash(state);
129 groups.hash(state);
130 marker.hash(state);
131 source.hash(state);
132 }
133}
134
135impl PartialEq for Requirement {
136 fn eq(&self, other: &Self) -> bool {
137 let Self {
138 name,
139 extras,
140 groups,
141 marker,
142 source,
143 origin: _,
144 } = self;
145 let Self {
146 name: other_name,
147 extras: other_extras,
148 groups: other_groups,
149 marker: other_marker,
150 source: other_source,
151 origin: _,
152 } = other;
153 name == other_name
154 && extras == other_extras
155 && groups == other_groups
156 && marker == other_marker
157 && source == other_source
158 }
159}
160
161impl Eq for Requirement {}
162
163impl Ord for Requirement {
164 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
165 let Self {
166 name,
167 extras,
168 groups,
169 marker,
170 source,
171 origin: _,
172 } = self;
173 let Self {
174 name: other_name,
175 extras: other_extras,
176 groups: other_groups,
177 marker: other_marker,
178 source: other_source,
179 origin: _,
180 } = other;
181 name.cmp(other_name)
182 .then_with(|| extras.cmp(other_extras))
183 .then_with(|| groups.cmp(other_groups))
184 .then_with(|| marker.cmp(other_marker))
185 .then_with(|| source.cmp(other_source))
186 }
187}
188
189impl PartialOrd for Requirement {
190 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
191 Some(self.cmp(other))
192 }
193}
194
195impl From<Requirement> for uv_pep508::Requirement<VerbatimUrl> {
196 fn from(requirement: Requirement) -> Self {
198 Self {
199 name: requirement.name,
200 extras: requirement.extras,
201 marker: requirement.marker,
202 origin: requirement.origin,
203 version_or_url: match requirement.source {
204 RequirementSource::Registry { specifier, .. } => {
205 Some(VersionOrUrl::VersionSpecifier(specifier))
206 }
207 RequirementSource::Url { url, .. }
208 | RequirementSource::Git { url, .. }
209 | RequirementSource::Path { url, .. }
210 | RequirementSource::Directory { url, .. } => Some(VersionOrUrl::Url(url)),
211 },
212 }
213 }
214}
215
216impl From<Requirement> for uv_pep508::Requirement<VerbatimParsedUrl> {
217 fn from(requirement: Requirement) -> Self {
219 Self {
220 name: requirement.name,
221 extras: requirement.extras,
222 marker: requirement.marker,
223 origin: requirement.origin,
224 version_or_url: match requirement.source {
225 RequirementSource::Registry { specifier, .. } => {
226 Some(VersionOrUrl::VersionSpecifier(specifier))
227 }
228 RequirementSource::Url {
229 location,
230 subdirectory,
231 ext,
232 url,
233 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
234 parsed_url: ParsedUrl::Archive(ParsedArchiveUrl {
235 url: location,
236 subdirectory,
237 ext,
238 }),
239 verbatim: url,
240 })),
241 RequirementSource::Git {
242 git,
243 subdirectory,
244 url,
245 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
246 parsed_url: ParsedUrl::Git(ParsedGitUrl {
247 url: git,
248 subdirectory,
249 }),
250 verbatim: url,
251 })),
252 RequirementSource::Path {
253 install_path,
254 ext,
255 url,
256 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
257 parsed_url: ParsedUrl::Path(ParsedPathUrl {
258 url: url.to_url(),
259 install_path,
260 ext,
261 }),
262 verbatim: url,
263 })),
264 RequirementSource::Directory {
265 install_path,
266 editable,
267 r#virtual,
268 url,
269 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
270 parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
271 url: url.to_url(),
272 install_path,
273 editable,
274 r#virtual,
275 }),
276 verbatim: url,
277 })),
278 },
279 }
280 }
281}
282
283impl From<uv_pep508::Requirement<VerbatimParsedUrl>> for Requirement {
284 fn from(requirement: uv_pep508::Requirement<VerbatimParsedUrl>) -> Self {
286 let source = match requirement.version_or_url {
287 None => RequirementSource::Registry {
288 specifier: VersionSpecifiers::empty(),
289 index: None,
290 conflict: None,
291 },
292 Some(VersionOrUrl::VersionSpecifier(specifier)) => RequirementSource::Registry {
294 specifier,
295 index: None,
296 conflict: None,
297 },
298 Some(VersionOrUrl::Url(url)) => {
299 RequirementSource::from_parsed_url(url.parsed_url, url.verbatim)
300 }
301 };
302 Self {
303 name: requirement.name,
304 groups: Box::new([]),
305 extras: requirement.extras,
306 marker: requirement.marker,
307 source,
308 origin: requirement.origin,
309 }
310 }
311}
312
313impl Display for Requirement {
314 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
317 write!(f, "{}", self.name)?;
318 if !self.extras.is_empty() {
319 write!(
320 f,
321 "[{}]",
322 self.extras
323 .iter()
324 .map(ToString::to_string)
325 .collect::<Vec<_>>()
326 .join(",")
327 )?;
328 }
329 match &self.source {
330 RequirementSource::Registry {
331 specifier, index, ..
332 } => {
333 write!(f, "{specifier}")?;
334 if let Some(index) = index {
335 write!(f, " (index: {})", index.url)?;
336 }
337 }
338 RequirementSource::Url { url, .. } => {
339 write!(f, " @ {url}")?;
340 }
341 RequirementSource::Git {
342 url: _,
343 git,
344 subdirectory,
345 } => {
346 write!(f, " @ git+{}", git.repository())?;
347 if let Some(reference) = git.reference().as_str() {
348 write!(f, "@{reference}")?;
349 }
350 if let Some(subdirectory) = subdirectory {
351 writeln!(f, "#subdirectory={}", subdirectory.display())?;
352 }
353 if git.lfs().enabled() {
354 writeln!(
355 f,
356 "{}lfs=true",
357 if subdirectory.is_some() { "&" } else { "#" }
358 )?;
359 }
360 }
361 RequirementSource::Path { url, .. } => {
362 write!(f, " @ {url}")?;
363 }
364 RequirementSource::Directory { url, .. } => {
365 write!(f, " @ {url}")?;
366 }
367 }
368 if let Some(marker) = self.marker.contents() {
369 write!(f, " ; {marker}")?;
370 }
371 Ok(())
372 }
373}
374
375impl CacheKey for Requirement {
376 fn cache_key(&self, state: &mut CacheKeyHasher) {
377 self.name.as_str().cache_key(state);
378
379 self.groups.len().cache_key(state);
380 for group in &self.groups {
381 group.as_str().cache_key(state);
382 }
383
384 self.extras.len().cache_key(state);
385 for extra in &self.extras {
386 extra.as_str().cache_key(state);
387 }
388
389 if let Some(marker) = self.marker.contents() {
390 1u8.cache_key(state);
391 marker.to_string().cache_key(state);
392 } else {
393 0u8.cache_key(state);
394 }
395
396 match &self.source {
397 RequirementSource::Registry {
398 specifier,
399 index,
400 conflict: _,
401 } => {
402 0u8.cache_key(state);
403 specifier.len().cache_key(state);
404 for spec in specifier.iter() {
405 spec.operator().as_str().cache_key(state);
406 spec.version().cache_key(state);
407 }
408 if let Some(index) = index {
409 1u8.cache_key(state);
410 index.url.cache_key(state);
411 } else {
412 0u8.cache_key(state);
413 }
414 }
416 RequirementSource::Url {
417 location,
418 subdirectory,
419 ext,
420 url,
421 } => {
422 1u8.cache_key(state);
423 location.cache_key(state);
424 if let Some(subdirectory) = subdirectory {
425 1u8.cache_key(state);
426 subdirectory.display().to_string().cache_key(state);
427 } else {
428 0u8.cache_key(state);
429 }
430 ext.name().cache_key(state);
431 url.cache_key(state);
432 }
433 RequirementSource::Git {
434 git,
435 subdirectory,
436 url,
437 } => {
438 2u8.cache_key(state);
439 git.to_string().cache_key(state);
440 if let Some(subdirectory) = subdirectory {
441 1u8.cache_key(state);
442 subdirectory.display().to_string().cache_key(state);
443 } else {
444 0u8.cache_key(state);
445 }
446 if git.lfs().enabled() {
447 1u8.cache_key(state);
448 }
449 url.cache_key(state);
450 }
451 RequirementSource::Path {
452 install_path,
453 ext,
454 url,
455 } => {
456 3u8.cache_key(state);
457 install_path.cache_key(state);
458 ext.name().cache_key(state);
459 url.cache_key(state);
460 }
461 RequirementSource::Directory {
462 install_path,
463 editable,
464 r#virtual,
465 url,
466 } => {
467 4u8.cache_key(state);
468 install_path.cache_key(state);
469 editable.cache_key(state);
470 r#virtual.cache_key(state);
471 url.cache_key(state);
472 }
473 }
474
475 }
477}
478
479#[derive(
486 Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
487)]
488#[serde(try_from = "RequirementSourceWire", into = "RequirementSourceWire")]
489pub enum RequirementSource {
490 Registry {
492 specifier: VersionSpecifiers,
493 index: Option<IndexMetadata>,
495 conflict: Option<ConflictItem>,
497 },
498 Url {
504 location: DisplaySafeUrl,
506 subdirectory: Option<Box<Path>>,
509 ext: DistExtension,
511 url: VerbatimUrl,
514 },
515 Git {
517 git: GitUrl,
519 subdirectory: Option<Box<Path>>,
521 url: VerbatimUrl,
524 },
525 Path {
529 install_path: Box<Path>,
531 ext: DistExtension,
533 url: VerbatimUrl,
536 },
537 Directory {
540 install_path: Box<Path>,
542 editable: Option<bool>,
544 r#virtual: Option<bool>,
546 url: VerbatimUrl,
549 },
550}
551
552impl RequirementSource {
553 pub fn from_parsed_url(parsed_url: ParsedUrl, url: VerbatimUrl) -> Self {
556 match parsed_url {
557 ParsedUrl::Path(local_file) => Self::Path {
558 install_path: local_file.install_path.clone(),
559 ext: local_file.ext,
560 url,
561 },
562 ParsedUrl::Directory(directory) => Self::Directory {
563 install_path: directory.install_path.clone(),
564 editable: directory.editable,
565 r#virtual: directory.r#virtual,
566 url,
567 },
568 ParsedUrl::Git(git) => Self::Git {
569 git: git.url.clone(),
570 url,
571 subdirectory: git.subdirectory,
572 },
573 ParsedUrl::Archive(archive) => Self::Url {
574 url,
575 location: archive.url,
576 subdirectory: archive.subdirectory,
577 ext: archive.ext,
578 },
579 }
580 }
581
582 pub fn to_verbatim_parsed_url(&self) -> Option<VerbatimParsedUrl> {
584 match self {
585 Self::Registry { .. } => None,
586 Self::Url {
587 location,
588 subdirectory,
589 ext,
590 url,
591 } => Some(VerbatimParsedUrl {
592 parsed_url: ParsedUrl::Archive(ParsedArchiveUrl::from_source(
593 location.clone(),
594 subdirectory.clone(),
595 *ext,
596 )),
597 verbatim: url.clone(),
598 }),
599 Self::Path {
600 install_path,
601 ext,
602 url,
603 } => Some(VerbatimParsedUrl {
604 parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source(
605 install_path.clone(),
606 *ext,
607 url.to_url(),
608 )),
609 verbatim: url.clone(),
610 }),
611 Self::Directory {
612 install_path,
613 editable,
614 r#virtual,
615 url,
616 } => Some(VerbatimParsedUrl {
617 parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
618 install_path.clone(),
619 *editable,
620 *r#virtual,
621 url.to_url(),
622 )),
623 verbatim: url.clone(),
624 }),
625 Self::Git {
626 git,
627 subdirectory,
628 url,
629 } => Some(VerbatimParsedUrl {
630 parsed_url: ParsedUrl::Git(ParsedGitUrl::from_source(
631 git.clone(),
632 subdirectory.clone(),
633 )),
634 verbatim: url.clone(),
635 }),
636 }
637 }
638
639 pub fn version_or_url(&self) -> Option<VersionOrUrl<VerbatimParsedUrl>> {
643 match self {
644 Self::Registry { specifier, .. } => {
645 if specifier.is_empty() {
646 None
647 } else {
648 Some(VersionOrUrl::VersionSpecifier(specifier.clone()))
649 }
650 }
651 Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
652 Some(VersionOrUrl::Url(self.to_verbatim_parsed_url()?))
653 }
654 }
655 }
656
657 pub fn is_editable(&self) -> bool {
659 matches!(
660 self,
661 Self::Directory {
662 editable: Some(true),
663 ..
664 }
665 )
666 }
667
668 pub fn is_empty(&self) -> bool {
670 match self {
671 Self::Registry { specifier, .. } => specifier.is_empty(),
672 Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
673 false
674 }
675 }
676 }
677
678 pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> {
680 match self {
681 Self::Registry { specifier, .. } => Some(specifier),
682 Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
683 None
684 }
685 }
686 }
687
688 pub fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
690 match self {
691 Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => Ok(self),
692 Self::Path {
693 install_path,
694 ext,
695 url,
696 } => Ok(Self::Path {
697 install_path: relative_to(&install_path, path)
698 .or_else(|_| std::path::absolute(install_path))?
699 .into_boxed_path(),
700 ext,
701 url,
702 }),
703 Self::Directory {
704 install_path,
705 editable,
706 r#virtual,
707 url,
708 ..
709 } => Ok(Self::Directory {
710 install_path: relative_to(&install_path, path)
711 .or_else(|_| std::path::absolute(install_path))?
712 .into_boxed_path(),
713 editable,
714 r#virtual,
715 url,
716 }),
717 }
718 }
719
720 #[must_use]
722 pub fn to_absolute(self, root: &Path) -> Self {
723 match self {
724 Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => self,
725 Self::Path {
726 install_path,
727 ext,
728 url,
729 } => Self::Path {
730 install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(),
731 ext,
732 url,
733 },
734 Self::Directory {
735 install_path,
736 editable,
737 r#virtual,
738 url,
739 ..
740 } => Self::Directory {
741 install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(),
742 editable,
743 r#virtual,
744 url,
745 },
746 }
747 }
748}
749
750impl Display for RequirementSource {
751 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
754 match self {
755 Self::Registry {
756 specifier, index, ..
757 } => {
758 write!(f, "{specifier}")?;
759 if let Some(index) = index {
760 write!(f, " (index: {})", index.url)?;
761 }
762 }
763 Self::Url { url, .. } => {
764 write!(f, " {url}")?;
765 }
766 Self::Git {
767 url: _,
768 git,
769 subdirectory,
770 } => {
771 write!(f, " git+{}", git.repository())?;
772 if let Some(reference) = git.reference().as_str() {
773 write!(f, "@{reference}")?;
774 }
775 if let Some(subdirectory) = subdirectory {
776 writeln!(f, "#subdirectory={}", subdirectory.display())?;
777 }
778 if git.lfs().enabled() {
779 writeln!(
780 f,
781 "{}lfs=true",
782 if subdirectory.is_some() { "&" } else { "#" }
783 )?;
784 }
785 }
786 Self::Path { url, .. } => {
787 write!(f, "{url}")?;
788 }
789 Self::Directory { url, .. } => {
790 write!(f, "{url}")?;
791 }
792 }
793 Ok(())
794 }
795}
796
797#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
798#[serde(untagged)]
799enum RequirementSourceWire {
800 Git { git: String },
802 Direct {
804 url: DisplaySafeUrl,
805 subdirectory: Option<PortablePathBuf>,
806 },
807 Path { path: PortablePathBuf },
809 Directory { directory: PortablePathBuf },
811 Editable { editable: PortablePathBuf },
813 Virtual { r#virtual: PortablePathBuf },
815 Registry {
817 #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)]
818 specifier: VersionSpecifiers,
819 index: Option<DisplaySafeUrl>,
820 conflict: Option<ConflictItem>,
821 },
822}
823
824impl From<RequirementSource> for RequirementSourceWire {
825 fn from(value: RequirementSource) -> Self {
826 match value {
827 RequirementSource::Registry {
828 specifier,
829 index,
830 conflict,
831 } => {
832 let index = index.map(|index| index.url.into_url()).map(|mut index| {
833 index.remove_credentials();
834 index
835 });
836 Self::Registry {
837 specifier,
838 index,
839 conflict,
840 }
841 }
842 RequirementSource::Url {
843 subdirectory,
844 location,
845 ext: _,
846 url: _,
847 } => Self::Direct {
848 url: location,
849 subdirectory: subdirectory.map(PortablePathBuf::from),
850 },
851 RequirementSource::Git {
852 git,
853 subdirectory,
854 url: _,
855 } => {
856 let mut url = git.repository().clone();
857
858 url.remove_credentials();
860
861 url.set_fragment(None);
863 url.set_query(None);
864
865 if let Some(subdirectory) = subdirectory
867 .as_deref()
868 .map(PortablePath::from)
869 .as_ref()
870 .map(PortablePath::to_string)
871 {
872 url.query_pairs_mut()
873 .append_pair("subdirectory", &subdirectory);
874 }
875
876 if git.lfs().enabled() {
878 url.query_pairs_mut().append_pair("lfs", "true");
879 }
880
881 match git.reference() {
883 GitReference::Branch(branch) => {
884 url.query_pairs_mut().append_pair("branch", branch.as_str());
885 }
886 GitReference::Tag(tag) => {
887 url.query_pairs_mut().append_pair("tag", tag.as_str());
888 }
889 GitReference::BranchOrTag(rev)
890 | GitReference::BranchOrTagOrCommit(rev)
891 | GitReference::NamedRef(rev) => {
892 url.query_pairs_mut().append_pair("rev", rev.as_str());
893 }
894 GitReference::DefaultBranch => {}
895 }
896
897 if let Some(precise) = git.precise() {
899 url.set_fragment(Some(&precise.to_string()));
900 }
901
902 Self::Git {
903 git: url.to_string(),
904 }
905 }
906 RequirementSource::Path {
907 install_path,
908 ext: _,
909 url: _,
910 } => Self::Path {
911 path: PortablePathBuf::from(install_path),
912 },
913 RequirementSource::Directory {
914 install_path,
915 editable,
916 r#virtual,
917 url: _,
918 } => {
919 if editable.unwrap_or(false) {
920 Self::Editable {
921 editable: PortablePathBuf::from(install_path),
922 }
923 } else if r#virtual.unwrap_or(false) {
924 Self::Virtual {
925 r#virtual: PortablePathBuf::from(install_path),
926 }
927 } else {
928 Self::Directory {
929 directory: PortablePathBuf::from(install_path),
930 }
931 }
932 }
933 }
934 }
935}
936
937impl TryFrom<RequirementSourceWire> for RequirementSource {
938 type Error = RequirementError;
939
940 fn try_from(wire: RequirementSourceWire) -> Result<Self, RequirementError> {
941 match wire {
942 RequirementSourceWire::Registry {
943 specifier,
944 index,
945 conflict,
946 } => Ok(Self::Registry {
947 specifier,
948 index: index
949 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))),
950 conflict,
951 }),
952 RequirementSourceWire::Git { git } => {
953 let mut repository = DisplaySafeUrl::parse(&git)?;
954
955 let mut reference = GitReference::DefaultBranch;
956 let mut subdirectory: Option<PortablePathBuf> = None;
957 let mut lfs = GitLfs::Disabled;
958 for (key, val) in repository.query_pairs() {
959 match &*key {
960 "tag" => reference = GitReference::Tag(val.into_owned()),
961 "branch" => reference = GitReference::Branch(val.into_owned()),
962 "rev" => reference = GitReference::from_rev(val.into_owned()),
963 "subdirectory" => {
964 subdirectory = Some(PortablePathBuf::from(val.as_ref()));
965 }
966 "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
967 _ => {}
968 }
969 }
970
971 let precise = repository.fragment().map(GitOid::from_str).transpose()?;
972
973 repository.set_fragment(None);
975 repository.set_query(None);
976
977 repository.remove_credentials();
979
980 let mut url = DisplaySafeUrl::parse(&format!("git+{repository}"))?;
982 if let Some(rev) = reference.as_str() {
983 let path = format!("{}@{}", url.path(), rev);
984 url.set_path(&path);
985 }
986 let mut frags: Vec<String> = Vec::new();
987 if let Some(subdirectory) = subdirectory.as_ref() {
988 frags.push(format!("subdirectory={subdirectory}"));
989 }
990 if lfs.enabled() {
992 frags.push("lfs=true".to_string());
993 }
994 if !frags.is_empty() {
995 url.set_fragment(Some(&frags.join("&")));
996 }
997
998 let url = VerbatimUrl::from_url(url);
999
1000 Ok(Self::Git {
1001 git: GitUrl::from_fields(repository, reference, precise, lfs)?,
1002 subdirectory: subdirectory.map(Box::<Path>::from),
1003 url,
1004 })
1005 }
1006 RequirementSourceWire::Direct { url, subdirectory } => {
1007 let location = url.clone();
1008
1009 let mut url = url.clone();
1011 if let Some(subdirectory) = &subdirectory {
1012 url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
1013 }
1014
1015 Ok(Self::Url {
1016 location,
1017 subdirectory: subdirectory.map(Box::<Path>::from),
1018 ext: DistExtension::from_path(url.path())
1019 .map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?,
1020 url: VerbatimUrl::from_url(url.clone()),
1021 })
1022 }
1023 RequirementSourceWire::Path { path } => {
1028 let path = Box::<Path>::from(path);
1029 let url =
1030 VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(CWD.join(&path)))?;
1031 Ok(Self::Path {
1032 ext: DistExtension::from_path(&path).map_err(|err| {
1033 ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err)
1034 })?,
1035 install_path: path,
1036 url,
1037 })
1038 }
1039 RequirementSourceWire::Directory { directory } => {
1040 let directory = Box::<Path>::from(directory);
1041 let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1042 CWD.join(&directory),
1043 ))?;
1044 Ok(Self::Directory {
1045 install_path: directory,
1046 editable: Some(false),
1047 r#virtual: Some(false),
1048 url,
1049 })
1050 }
1051 RequirementSourceWire::Editable { editable } => {
1052 let editable = Box::<Path>::from(editable);
1053 let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1054 CWD.join(&editable),
1055 ))?;
1056 Ok(Self::Directory {
1057 install_path: editable,
1058 editable: Some(true),
1059 r#virtual: Some(false),
1060 url,
1061 })
1062 }
1063 RequirementSourceWire::Virtual { r#virtual } => {
1064 let r#virtual = Box::<Path>::from(r#virtual);
1065 let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1066 CWD.join(&r#virtual),
1067 ))?;
1068 Ok(Self::Directory {
1069 install_path: r#virtual,
1070 editable: Some(false),
1071 r#virtual: Some(true),
1072 url,
1073 })
1074 }
1075 }
1076 }
1077}
1078
1079#[cfg(test)]
1080mod tests {
1081 use std::path::PathBuf;
1082
1083 use uv_pep508::{MarkerTree, VerbatimUrl};
1084
1085 use crate::{Requirement, RequirementSource};
1086
1087 #[test]
1088 fn roundtrip() {
1089 let requirement = Requirement {
1090 name: "foo".parse().unwrap(),
1091 extras: Box::new([]),
1092 groups: Box::new([]),
1093 marker: MarkerTree::TRUE,
1094 source: RequirementSource::Registry {
1095 specifier: ">1,<2".parse().unwrap(),
1096 index: None,
1097 conflict: None,
1098 },
1099 origin: None,
1100 };
1101
1102 let raw = toml::to_string(&requirement).unwrap();
1103 let deserialized: Requirement = toml::from_str(&raw).unwrap();
1104 assert_eq!(requirement, deserialized);
1105
1106 let path = if cfg!(windows) {
1107 "C:\\home\\ferris\\foo"
1108 } else {
1109 "/home/ferris/foo"
1110 };
1111 let requirement = Requirement {
1112 name: "foo".parse().unwrap(),
1113 extras: Box::new([]),
1114 groups: Box::new([]),
1115 marker: MarkerTree::TRUE,
1116 source: RequirementSource::Directory {
1117 install_path: PathBuf::from(path).into_boxed_path(),
1118 editable: Some(false),
1119 r#virtual: Some(false),
1120 url: VerbatimUrl::from_absolute_path(path).unwrap(),
1121 },
1122 origin: None,
1123 };
1124
1125 let raw = toml::to_string(&requirement).unwrap();
1126 let deserialized: Requirement = toml::from_str(&raw).unwrap();
1127 assert_eq!(requirement, deserialized);
1128 }
1129}