1use std::collections::BTreeMap;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use either::Either;
6use owo_colors::OwoColorize;
7use thiserror::Error;
8use uv_auth::CredentialsCache;
9use uv_distribution_filename::DistExtension;
10use uv_distribution_types::{
11 Index, IndexLocations, IndexMetadata, IndexName, Origin, Requirement, RequirementSource,
12};
13use uv_fs::{Simplified, normalize_absolute_path, normalize_path};
14use uv_git_types::{GitLfs, GitReference, GitUrl, GitUrlParseError};
15use uv_normalize::{ExtraName, GroupName, PackageName};
16use uv_pep440::VersionSpecifiers;
17use uv_pep508::{MarkerTree, VerbatimUrl, VersionOrUrl, looks_like_git_repository};
18use uv_pypi_types::{ConflictItem, ParsedGitUrl, ParsedUrl, ParsedUrlError, VerbatimParsedUrl};
19use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
20use uv_workspace::Workspace;
21use uv_workspace::pyproject::{PyProjectToml, Source, Sources};
22
23use crate::metadata::GitWorkspaceMember;
24
25#[derive(Debug, Clone)]
26pub struct LoweredRequirement(Requirement);
27
28#[derive(Debug, Clone, Copy)]
29enum RequirementOrigin {
30 Project,
32 Workspace,
34}
35
36impl LoweredRequirement {
37 pub(crate) fn from_requirement<'data>(
39 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
40 project_name: Option<&'data PackageName>,
41 project_dir: &'data Path,
42 project_sources: &'data BTreeMap<PackageName, Sources>,
43 project_indexes: &'data [Index],
44 extra: Option<&ExtraName>,
45 group: Option<&GroupName>,
46 locations: &'data IndexLocations,
47 workspace: &'data Workspace,
48 git_member: Option<&'data GitWorkspaceMember<'data>>,
49 editable: bool,
50 credentials_cache: &'data CredentialsCache,
51 ) -> impl Iterator<Item = Result<Self, LoweringError>> + use<'data> + 'data {
52 let (sources, origin) = if let Some(source) = project_sources.get(&requirement.name) {
54 (Some(source), RequirementOrigin::Project)
55 } else if let Some(source) = workspace.sources().get(&requirement.name) {
56 (Some(source), RequirementOrigin::Workspace)
57 } else {
58 (None, RequirementOrigin::Project)
59 };
60
61 let sources = sources.map(|sources| {
63 sources
64 .iter()
65 .filter(|source| {
66 if let Some(target) = source.extra() {
67 if extra != Some(target) {
68 return false;
69 }
70 }
71
72 if let Some(target) = source.group() {
73 if group != Some(target) {
74 return false;
75 }
76 }
77
78 true
79 })
80 .cloned()
81 .collect::<Sources>()
82 });
83
84 if workspace.packages().contains_key(&requirement.name) {
86 if project_name.is_none_or(|project_name| *project_name != requirement.name) {
89 let Some(sources) = sources.as_ref() else {
91 return Either::Left(std::iter::once(Err(
93 LoweringError::MissingWorkspaceSource(requirement.name.clone()),
94 )));
95 };
96
97 for source in sources.iter() {
98 match source {
99 Source::Git { .. } => {
100 return Either::Left(std::iter::once(Err(
101 LoweringError::NonWorkspaceSource(
102 requirement.name.clone(),
103 SourceKind::Git,
104 ),
105 )));
106 }
107 Source::Url { .. } => {
108 return Either::Left(std::iter::once(Err(
109 LoweringError::NonWorkspaceSource(
110 requirement.name.clone(),
111 SourceKind::Url,
112 ),
113 )));
114 }
115 Source::Path { .. } => {
116 return Either::Left(std::iter::once(Err(
117 LoweringError::NonWorkspaceSource(
118 requirement.name.clone(),
119 SourceKind::Path,
120 ),
121 )));
122 }
123 Source::Registry { .. } => {
124 return Either::Left(std::iter::once(Err(
125 LoweringError::NonWorkspaceSource(
126 requirement.name.clone(),
127 SourceKind::Registry,
128 ),
129 )));
130 }
131 Source::Workspace { .. } => {
132 }
134 }
135 }
136 }
137 }
138
139 let Some(sources) = sources else {
140 return Either::Left(std::iter::once(Self::preserve_git_source(
141 requirement,
142 git_member,
143 )));
144 };
145
146 let remaining = {
149 let mut total = MarkerTree::FALSE;
151 for source in sources.iter() {
152 total.or(source.marker());
153 }
154
155 let mut remaining = total.negate();
157 remaining.and(requirement.marker);
158
159 Self(Requirement {
160 marker: remaining,
161 ..Requirement::from(requirement.clone())
162 })
163 };
164
165 Either::Right(
166 sources
167 .into_iter()
168 .map(move |source| {
169 let (source, mut marker) = match source {
170 Source::Git {
171 git,
172 subdirectory,
173 rev,
174 tag,
175 branch,
176 lfs,
177 marker,
178 ..
179 } => {
180 let source = git_source(
181 &git,
182 subdirectory.map(Box::<Path>::from),
183 rev,
184 tag,
185 branch,
186 lfs,
187 )?;
188 (source, marker)
189 }
190 Source::Url {
191 url,
192 subdirectory,
193 marker,
194 ..
195 } => {
196 let source =
197 url_source(&requirement, url, subdirectory.map(Box::<Path>::from))?;
198 (source, marker)
199 }
200 Source::Path {
201 path,
202 editable,
203 package,
204 marker,
205 ..
206 } => {
207 let source = path_source(
208 path,
209 git_member,
210 origin,
211 project_dir,
212 workspace.install_path(),
213 editable,
214 package,
215 )?;
216 (source, marker)
217 }
218 Source::Registry {
219 index,
220 marker,
221 extra,
222 group,
223 } => {
224 let Some(index) = locations
227 .indexes()
228 .filter(|index| matches!(index.origin, Some(Origin::Cli)))
229 .chain(project_indexes.iter())
230 .chain(workspace.indexes().iter())
231 .find(|Index { name, .. }| {
232 name.as_ref().is_some_and(|name| *name == index)
233 })
234 else {
235 let hint = missing_index_hint(locations, &index);
236 return Err(LoweringError::MissingIndex {
237 package: requirement.name.clone(),
238 index,
239 hint,
240 });
241 };
242 if let Some(credentials) = index.credentials() {
243 credentials_cache.store_credentials(index.raw_url(), credentials);
244 }
245 let index = IndexMetadata {
246 url: index.url.clone(),
247 format: index.format,
248 };
249 let conflict = project_name.and_then(|project_name| {
250 if let Some(extra) = extra {
251 Some(ConflictItem::from((project_name.clone(), extra)))
252 } else {
253 group.map(|group| {
254 ConflictItem::from((project_name.clone(), group))
255 })
256 }
257 });
258 let source = registry_source(&requirement, index, conflict);
259 (source, marker)
260 }
261 Source::Workspace {
262 workspace: is_workspace,
263 marker,
264 ..
265 } => {
266 if !is_workspace {
267 return Err(LoweringError::WorkspaceFalse);
268 }
269 let member = workspace
270 .packages()
271 .get(&requirement.name)
272 .ok_or_else(|| {
273 LoweringError::UndeclaredWorkspacePackage(
274 requirement.name.clone(),
275 )
276 })?
277 .clone();
278
279 let url = VerbatimUrl::from_absolute_path(member.root())?;
297 let install_path = url.to_file_path().map_err(|()| {
298 LoweringError::RelativeTo(io::Error::other(
299 "Invalid path in file URL",
300 ))
301 })?;
302
303 let source = if let Some(git_member) = &git_member {
304 let subdirectory =
307 uv_fs::relative_to(member.root(), git_member.fetch_root)
308 .expect("Workspace member must be relative");
309 let subdirectory = normalize_path(subdirectory);
310 RequirementSource::Git {
311 git: git_member.git_source.git.clone(),
312 subdirectory: if subdirectory == PathBuf::new() {
313 None
314 } else {
315 Some(subdirectory.into_owned().into_boxed_path())
316 },
317 url,
318 }
319 } else {
320 let value = workspace.required_members().get(&requirement.name);
321 let is_required_member = value.is_some();
322 let editability = value.copied().flatten();
323 if member.pyproject_toml().is_package(!is_required_member) {
324 RequirementSource::Directory {
325 install_path: install_path.into_boxed_path(),
326 url,
327 editable: Some(editability.unwrap_or(editable)),
328 r#virtual: Some(false),
329 }
330 } else {
331 RequirementSource::Directory {
332 install_path: install_path.into_boxed_path(),
333 url,
334 editable: Some(false),
335 r#virtual: Some(true),
336 }
337 }
338 };
339 (source, marker)
340 }
341 };
342
343 marker.and(requirement.marker);
344
345 Ok(Self(Requirement {
346 name: requirement.name.clone(),
347 extras: requirement.extras.clone(),
348 groups: Box::new([]),
349 marker,
350 source,
351 origin: requirement.origin.clone(),
352 }))
353 })
354 .chain(std::iter::once(Ok(remaining)))
355 .filter(|requirement| match requirement {
356 Ok(requirement) => !requirement.0.marker.is_false(),
357 Err(_) => true,
358 }),
359 )
360 }
361
362 pub fn from_non_workspace_requirement<'data>(
365 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
366 dir: &'data Path,
367 sources: &'data BTreeMap<PackageName, Sources>,
368 indexes: &'data [Index],
369 locations: &'data IndexLocations,
370 credentials_cache: &'data CredentialsCache,
371 ) -> impl Iterator<Item = Result<Self, LoweringError>> + 'data {
372 let source = sources.get(&requirement.name).cloned();
373
374 let Some(source) = source else {
375 return Either::Left(std::iter::once(Ok(Self(Requirement::from(requirement)))));
376 };
377
378 let source = source
380 .iter()
381 .filter(|source| {
382 source.extra().is_none_or(|target| {
383 requirement
384 .marker
385 .top_level_extra_name()
386 .is_some_and(|extra| &*extra == target)
387 })
388 })
389 .cloned()
390 .collect::<Sources>();
391
392 let remaining = {
395 let mut total = MarkerTree::FALSE;
397 for source in source.iter() {
398 total.or(source.marker());
399 }
400
401 let mut remaining = total.negate();
403 remaining.and(requirement.marker);
404
405 Self(Requirement {
406 marker: remaining,
407 ..Requirement::from(requirement.clone())
408 })
409 };
410
411 Either::Right(
412 source
413 .into_iter()
414 .map(move |source| {
415 let (source, mut marker) = match source {
416 Source::Git {
417 git,
418 subdirectory,
419 rev,
420 tag,
421 branch,
422 lfs,
423 marker,
424 ..
425 } => {
426 let source = git_source(
427 &git,
428 subdirectory.map(Box::<Path>::from),
429 rev,
430 tag,
431 branch,
432 lfs,
433 )?;
434 (source, marker)
435 }
436 Source::Url {
437 url,
438 subdirectory,
439 marker,
440 ..
441 } => {
442 let source =
443 url_source(&requirement, url, subdirectory.map(Box::<Path>::from))?;
444 (source, marker)
445 }
446 Source::Path {
447 path,
448 editable,
449 package,
450 marker,
451 ..
452 } => {
453 let source = path_source(
454 path,
455 None,
456 RequirementOrigin::Project,
457 dir,
458 dir,
459 editable,
460 package,
461 )?;
462 (source, marker)
463 }
464 Source::Registry { index, marker, .. } => {
465 let Some(index) = locations
466 .indexes()
467 .filter(|index| matches!(index.origin, Some(Origin::Cli)))
468 .chain(indexes.iter())
469 .find(|Index { name, .. }| {
470 name.as_ref().is_some_and(|name| *name == index)
471 })
472 else {
473 let hint = missing_index_hint(locations, &index);
474 return Err(LoweringError::MissingIndex {
475 package: requirement.name.clone(),
476 index,
477 hint,
478 });
479 };
480 if let Some(credentials) = index.credentials() {
481 credentials_cache.store_credentials(index.raw_url(), credentials);
482 }
483 let index = IndexMetadata {
484 url: index.url.clone(),
485 format: index.format,
486 };
487 let conflict = None;
488 let source = registry_source(&requirement, index, conflict);
489 (source, marker)
490 }
491 Source::Workspace { .. } => {
492 return Err(LoweringError::WorkspaceMember);
493 }
494 };
495
496 marker.and(requirement.marker);
497
498 Ok(Self(Requirement {
499 name: requirement.name.clone(),
500 extras: requirement.extras.clone(),
501 groups: Box::new([]),
502 marker,
503 source,
504 origin: requirement.origin.clone(),
505 }))
506 })
507 .chain(std::iter::once(Ok(remaining)))
508 .filter(|requirement| match requirement {
509 Ok(requirement) => !requirement.0.marker.is_false(),
510 Err(_) => true,
511 }),
512 )
513 }
514
515 pub(crate) fn preserve_git_source(
518 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
519 git_member: Option<&GitWorkspaceMember>,
520 ) -> Result<Self, LoweringError> {
521 let Some(git_member) = git_member else {
522 return Ok(Self(Requirement::from(requirement)));
523 };
524
525 let Some(VersionOrUrl::Url(url)) = &requirement.version_or_url else {
526 return Ok(Self(Requirement::from(requirement)));
527 };
528
529 let ParsedUrl::Directory(directory) = &url.parsed_url else {
530 return Ok(Self(Requirement::from(requirement)));
531 };
532
533 let install_path = git_path(&directory.install_path)?;
534 let fetch_root = git_path(git_member.fetch_root)?;
535 if !install_path.starts_with(&fetch_root) {
536 return Ok(Self(Requirement::from(requirement)));
537 }
538
539 Ok(Self(Requirement {
540 name: requirement.name,
541 groups: Box::new([]),
542 extras: requirement.extras,
543 marker: requirement.marker,
544 source: git_source_from_path(&install_path, git_member)?,
545 origin: requirement.origin,
546 }))
547 }
548
549 pub fn into_inner(self) -> Requirement {
551 self.0
552 }
553}
554
555#[derive(Debug, Error)]
558pub enum LoweringError {
559 #[error(
560 "`{0}` is included as a workspace member, but is missing an entry in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`)"
561 )]
562 MissingWorkspaceSource(PackageName),
563 #[error(
564 "`{0}` is included as a workspace member, but references a {1} in `tool.uv.sources`. Workspace members must be declared as workspace sources (e.g., `{0} = {{ workspace = true }}`)."
565 )]
566 NonWorkspaceSource(PackageName, SourceKind),
567 #[error(
568 "`{0}` references a workspace in `tool.uv.sources` (e.g., `{0} = {{ workspace = true }}`), but is not a workspace member"
569 )]
570 UndeclaredWorkspacePackage(PackageName),
571 #[error("Can only specify one of: `rev`, `tag`, or `branch`")]
572 MoreThanOneGitRef,
573 #[error(transparent)]
574 GitUrlParse(#[from] GitUrlParseError),
575 #[error("Package `{package}` references an undeclared index: `{index}`{}", if let Some(hint) = hint { format!("\n\n{}{} {hint}", "hint".bold().cyan(), ":".bold()) } else { String::new() })]
576 MissingIndex {
577 package: PackageName,
578 index: IndexName,
579 hint: Option<String>,
580 },
581 #[error("Workspace members are not allowed in non-workspace contexts")]
582 WorkspaceMember,
583 #[error(transparent)]
584 InvalidUrl(#[from] DisplaySafeUrlError),
585 #[error(transparent)]
586 InvalidVerbatimUrl(#[from] uv_pep508::VerbatimUrlError),
587 #[error("Fragments are not allowed in URLs: `{0}`")]
588 ForbiddenFragment(DisplaySafeUrl),
589 #[error(
590 "`{0}` is associated with a URL source, but references a Git repository. Consider using a Git source instead (e.g., `{0} = {{ git = \"{1}\" }}`)"
591 )]
592 MissingGitSource(PackageName, DisplaySafeUrl),
593 #[error("`workspace = false` is not yet supported")]
594 WorkspaceFalse,
595 #[error("Source with `editable = true` must refer to a local directory, not a file: `{0}`")]
596 EditableFile(String),
597 #[error("Source with `package = true` must refer to a local directory, not a file: `{0}`")]
598 PackagedFile(String),
599 #[error(
600 "Git repository references local file source, but only directories are supported as transitive Git dependencies: `{0}`"
601 )]
602 GitFile(String),
603 #[error(transparent)]
604 ParsedUrl(#[from] ParsedUrlError),
605 #[error("Path must be UTF-8: `{0}`")]
606 NonUtf8Path(PathBuf),
607 #[error(transparent)] RelativeTo(io::Error),
609}
610
611#[derive(Debug, Copy, Clone)]
612pub enum SourceKind {
613 Path,
614 Url,
615 Git,
616 Registry,
617}
618
619impl std::fmt::Display for SourceKind {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 match self {
622 Self::Path => write!(f, "path"),
623 Self::Url => write!(f, "URL"),
624 Self::Git => write!(f, "Git"),
625 Self::Registry => write!(f, "registry"),
626 }
627 }
628}
629
630fn missing_index_hint(locations: &IndexLocations, index: &IndexName) -> Option<String> {
633 let config_index = locations
634 .simple_indexes()
635 .filter(|idx| !matches!(idx.origin, Some(Origin::Cli)))
636 .find(|idx| idx.name.as_ref().is_some_and(|name| *name == *index));
637
638 config_index.and_then(|idx| {
639 let source = match idx.origin {
640 Some(Origin::User) => "a user-level `uv.toml`",
641 Some(Origin::System) => "a system-level `uv.toml`",
642 Some(Origin::Project) => "a project-level `uv.toml`",
643 Some(Origin::Cli | Origin::RequirementsTxt) | None => return None,
644 };
645 Some(format!(
646 "Index `{index}` was found in {source}, but indexes \
647 referenced via `tool.uv.sources` must be defined in the project's \
648 `pyproject.toml`"
649 ))
650 })
651}
652
653fn git_source(
655 git: &DisplaySafeUrl,
656 subdirectory: Option<Box<Path>>,
657 rev: Option<String>,
658 tag: Option<String>,
659 branch: Option<String>,
660 lfs: Option<bool>,
661) -> Result<RequirementSource, LoweringError> {
662 let reference = match (rev, tag, branch) {
663 (None, None, None) => GitReference::DefaultBranch,
664 (Some(rev), None, None) => GitReference::from_rev(rev),
665 (None, Some(tag), None) => GitReference::Tag(tag),
666 (None, None, Some(branch)) => GitReference::Branch(branch),
667 _ => return Err(LoweringError::MoreThanOneGitRef),
668 };
669
670 let mut url = DisplaySafeUrl::parse(&format!("git+{git}"))?;
672 if let Some(rev) = reference.as_str() {
673 let path = format!("{}@{}", url.path(), rev);
674 url.set_path(&path);
675 }
676 let mut frags: Vec<String> = Vec::new();
677 if let Some(subdirectory) = subdirectory.as_ref() {
678 let subdirectory = subdirectory
679 .to_str()
680 .ok_or_else(|| LoweringError::NonUtf8Path(subdirectory.to_path_buf()))?;
681 frags.push(format!("subdirectory={subdirectory}"));
682 }
683 let lfs = GitLfs::from(lfs);
687 if lfs.enabled() {
689 frags.push("lfs=true".to_string());
690 }
691 if !frags.is_empty() {
692 url.set_fragment(Some(&frags.join("&")));
693 }
694
695 let url = VerbatimUrl::from_url(url);
696
697 let repository = git.clone();
698
699 Ok(RequirementSource::Git {
700 url,
701 git: GitUrl::from_fields(repository, reference, None, lfs)?,
702 subdirectory,
703 })
704}
705
706fn url_source(
708 requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
709 url: DisplaySafeUrl,
710 subdirectory: Option<Box<Path>>,
711) -> Result<RequirementSource, LoweringError> {
712 let mut verbatim_url = url.clone();
713 if verbatim_url.fragment().is_some() {
714 return Err(LoweringError::ForbiddenFragment(url));
715 }
716 if let Some(subdirectory) = subdirectory.as_ref() {
717 let subdirectory = subdirectory
718 .to_str()
719 .ok_or_else(|| LoweringError::NonUtf8Path(subdirectory.to_path_buf()))?;
720 verbatim_url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
721 }
722
723 let ext = match DistExtension::from_path(url.path()) {
724 Ok(ext) => ext,
725 Err(..) if looks_like_git_repository(&url) => {
726 return Err(LoweringError::MissingGitSource(
727 requirement.name.clone(),
728 url.clone(),
729 ));
730 }
731 Err(err) => {
732 return Err(ParsedUrlError::MissingExtensionUrl(url.to_string(), err).into());
733 }
734 };
735
736 let verbatim_url = VerbatimUrl::from_url(verbatim_url);
737 Ok(RequirementSource::Url {
738 location: url,
739 subdirectory,
740 ext,
741 url: verbatim_url,
742 })
743}
744
745fn registry_source(
747 requirement: &uv_pep508::Requirement<VerbatimParsedUrl>,
748 index: IndexMetadata,
749 conflict: Option<ConflictItem>,
750) -> RequirementSource {
751 match &requirement.version_or_url {
752 None => RequirementSource::Registry {
753 specifier: VersionSpecifiers::empty(),
754 index: Some(index),
755 conflict,
756 },
757 Some(VersionOrUrl::VersionSpecifier(version)) => RequirementSource::Registry {
758 specifier: version.clone(),
759 index: Some(index),
760 conflict,
761 },
762 Some(VersionOrUrl::Url(_)) => RequirementSource::Registry {
763 specifier: VersionSpecifiers::empty(),
764 index: Some(index),
765 conflict,
766 },
767 }
768}
769
770fn path_source(
772 path: impl AsRef<Path>,
773 git_member: Option<&GitWorkspaceMember>,
774 origin: RequirementOrigin,
775 project_dir: &Path,
776 workspace_root: &Path,
777 editable: Option<bool>,
778 package: Option<bool>,
779) -> Result<RequirementSource, LoweringError> {
780 let path = path.as_ref();
781 let base = match origin {
782 RequirementOrigin::Project => project_dir,
783 RequirementOrigin::Workspace => workspace_root,
784 };
785 let url = VerbatimUrl::from_path(path, base)?.with_given(path.to_string_lossy());
786 let install_path = url
787 .to_file_path()
788 .map_err(|()| LoweringError::RelativeTo(io::Error::other("Invalid path in file URL")))?;
789
790 let is_dir = if let Ok(metadata) = install_path.metadata() {
791 metadata.is_dir()
792 } else {
793 install_path.extension().is_none()
794 };
795 if is_dir {
796 if let Some(git_member) = git_member {
797 return git_source_from_path(install_path, git_member);
798 }
799
800 if editable == Some(true) {
801 Ok(RequirementSource::Directory {
802 install_path: install_path.into_boxed_path(),
803 url,
804 editable,
805 r#virtual: Some(false),
806 })
807 } else {
808 let is_package = package.unwrap_or_else(|| {
812 let pyproject_path = install_path.join("pyproject.toml");
813 fs_err::read_to_string(&pyproject_path)
814 .ok()
815 .and_then(|contents| PyProjectToml::from_string(contents, pyproject_path).ok())
816 .map(|pyproject_toml| pyproject_toml.is_package(false))
818 .unwrap_or(true)
819 });
820
821 let r#virtual = !is_package;
823
824 Ok(RequirementSource::Directory {
825 install_path: install_path.into_boxed_path(),
826 url,
827 editable: Some(false),
828 r#virtual: Some(r#virtual),
829 })
830 }
831 } else {
832 if git_member.is_some() {
834 return Err(LoweringError::GitFile(url.to_string()));
835 }
836 if editable == Some(true) {
837 return Err(LoweringError::EditableFile(url.to_string()));
838 }
839 if package == Some(true) {
840 return Err(LoweringError::PackagedFile(url.to_string()));
841 }
842 Ok(RequirementSource::Path {
843 ext: DistExtension::from_path(&install_path)
844 .map_err(|err| ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err))?,
845 install_path: install_path.into_boxed_path(),
846 url,
847 })
848 }
849}
850
851fn git_source_from_path(
852 install_path: impl AsRef<Path>,
853 git_member: &GitWorkspaceMember,
854) -> Result<RequirementSource, LoweringError> {
855 let git = git_member.git_source.git.clone();
856 let install_path = git_path(install_path.as_ref())?;
857 let fetch_root = git_path(git_member.fetch_root)?;
858 let subdirectory =
859 uv_fs::relative_to(install_path, fetch_root).map_err(LoweringError::RelativeTo)?;
860 let subdirectory = normalize_path(subdirectory);
861 let subdirectory = if subdirectory == PathBuf::new() {
862 None
863 } else {
864 Some(subdirectory.into_owned().into_boxed_path())
865 };
866 let url = DisplaySafeUrl::from(ParsedGitUrl {
867 url: git.clone(),
868 subdirectory: subdirectory.clone(),
869 });
870 Ok(RequirementSource::Git {
871 git,
872 subdirectory,
873 url: VerbatimUrl::from_url(url),
874 })
875}
876
877fn git_path(path: &Path) -> Result<PathBuf, LoweringError> {
878 path.simple_canonicalize()
879 .or_else(|_| normalize_absolute_path(path))
880 .map_err(LoweringError::RelativeTo)
881}