Skip to main content

pray_core/
resolve_git_sources.rs

1use crate::lockfile::Lockfile;
2use crate::manifest::{ManifestPackage, ManifestSource};
3use crate::registry::{resolve_local_registry_package_root, RegistryPackageResolution};
4use crate::resolve_context::{PackageResolutionContext, ResolveOptions};
5use crate::resolve_git::{ensure_git_repository, local_git_source_root, resolve_distribution_root};
6use crate::{PrayError, PrayResult};
7use std::collections::BTreeMap;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
11pub(crate) struct GitSourceCheckout {
12    pub(crate) cache_directory: PathBuf,
13    pub(crate) revision: String,
14    pub(crate) subdir: Option<String>,
15}
16
17pub(crate) fn prepare_pray_ssh_host_keys(
18    sources: &[ManifestSource],
19) -> PrayResult<BTreeMap<String, String>> {
20    use crate::client_trust::{effective_trust_home, gate_pray_ssh_host};
21    use crate::ssh_client::parse_pray_ssh_url;
22
23    let home = effective_trust_home()?;
24    let mut host_keys = BTreeMap::new();
25    for source in sources {
26        if source.kind != "pray_ssh" {
27            continue;
28        }
29        let target = parse_pray_ssh_url(&source.url)?;
30        let fingerprint = gate_pray_ssh_host(&home, &source.url, &target.host, target.port)?;
31        if !fingerprint.is_empty() {
32            host_keys.insert(source.name.clone(), fingerprint);
33        }
34    }
35    Ok(host_keys)
36}
37
38pub(crate) fn prepare_git_sources(
39    project_root: &Path,
40    sources: &[ManifestSource],
41    lockfile: Option<&Lockfile>,
42    options: &ResolveOptions,
43) -> PrayResult<BTreeMap<String, GitSourceCheckout>> {
44    let mut git_sources = BTreeMap::new();
45    for source in sources {
46        if source.kind != "git" {
47            continue;
48        }
49        let clone_url = source.url.strip_prefix("git+").unwrap_or(&source.url);
50        let pinned_revision = if options.refresh_source_revisions {
51            None
52        } else {
53            pinned_revision_for_source(lockfile, source)
54        };
55        let refresh = options.refresh_source_revisions;
56        if is_local_filesystem_source(clone_url) && local_git_repo_path(clone_url).is_none() {
57            if let Some(source_root) = local_git_source_root(clone_url) {
58                git_sources.insert(
59                    source.name.clone(),
60                    GitSourceCheckout {
61                        cache_directory: source_root,
62                        revision: String::new(),
63                        subdir: source.subdir.clone(),
64                    },
65                );
66            }
67            continue;
68        }
69        let (cache_directory, revision) = ensure_git_repository(
70            project_root,
71            clone_url,
72            refresh,
73            pinned_revision.as_deref(),
74            source.subdir.as_deref(),
75        )?;
76        git_sources.insert(
77            source.name.clone(),
78            GitSourceCheckout {
79                cache_directory,
80                revision,
81                subdir: source.subdir.clone(),
82            },
83        );
84    }
85    Ok(git_sources)
86}
87
88pub(crate) fn is_local_filesystem_source(clone_url: &str) -> bool {
89    clone_url.starts_with("file://") || Path::new(clone_url).is_absolute()
90}
91
92pub(crate) fn local_git_repo_path(clone_url: &str) -> Option<PathBuf> {
93    let path = if let Some(path) = clone_url.strip_prefix("file://") {
94        PathBuf::from(path)
95    } else {
96        PathBuf::from(clone_url)
97    };
98    if path.join(".git").is_dir() {
99        Some(path)
100    } else {
101        None
102    }
103}
104
105pub(crate) fn pinned_revision_for_source(
106    lockfile: Option<&Lockfile>,
107    source: &ManifestSource,
108) -> Option<String> {
109    if let Some(revision) = lockfile
110        .and_then(|lockfile| {
111            lockfile
112                .source
113                .iter()
114                .find(|entry| entry.name == source.name && entry.kind == "git")
115        })
116        .and_then(|entry| entry.revision.clone())
117    {
118        return Some(revision);
119    }
120    if source.kind != "git" {
121        return None;
122    }
123    source.rev.clone().or_else(|| source.tag.clone())
124}
125
126pub(crate) fn resolve_git_package_root(
127    project_root: &Path,
128    source_name: &str,
129    source_url: &str,
130    git_sources: &BTreeMap<String, GitSourceCheckout>,
131    declaration: &ManifestPackage,
132    context: &PackageResolutionContext,
133) -> PrayResult<RegistryPackageResolution> {
134    let clone_url = source_url.strip_prefix("git+").unwrap_or(source_url);
135    if let Some(checkout) = git_sources.get(source_name) {
136        let distribution_root =
137            resolve_distribution_root(&checkout.cache_directory, checkout.subdir.as_deref())?;
138        let source_key = if checkout.revision.is_empty() {
139            clone_url.to_string()
140        } else {
141            format!("{}@{}", clone_url, checkout.revision)
142        };
143        return resolve_local_registry_package_root(
144            project_root,
145            &source_key,
146            &distribution_root,
147            declaration,
148            context,
149        )
150        .map_err(|error| {
151            crate::resolve_git_refresh::annotate_missing_git_catalog(
152                error,
153                &declaration.name,
154                source_name,
155                &checkout.revision,
156            )
157        });
158    }
159    if let Some(source_root) = local_git_source_root(clone_url) {
160        return resolve_local_registry_package_root(
161            project_root,
162            clone_url,
163            &source_root,
164            declaration,
165            context,
166        );
167    }
168    Err(PrayError::Resolution(format!(
169        "git source {source_name} was not prepared"
170    )))
171}
172
173pub fn refresh_git_sources(manifest_path: &Path) -> PrayResult<()> {
174    let project_root = project_root_for_manifest(manifest_path)?;
175    let manifest_text = crate::manifest::read_manifest_text(manifest_path)?;
176    let manifest = crate::manifest::parse_manifest(&manifest_text)?;
177    for source in &manifest.sources {
178        if source.kind != "git" {
179            continue;
180        }
181        let clone_url = source.url.strip_prefix("git+").unwrap_or(&source.url);
182        if is_local_filesystem_source(clone_url) && local_git_repo_path(clone_url).is_none() {
183            continue;
184        }
185        let _ = ensure_git_repository(
186            &project_root,
187            clone_url,
188            true,
189            None,
190            source.subdir.as_deref(),
191        )?;
192    }
193    Ok(())
194}
195
196fn project_root_for_manifest(manifest_path: &Path) -> PrayResult<PathBuf> {
197    let root = match manifest_path.parent() {
198        Some(parent) if !parent.as_os_str().is_empty() => parent.to_path_buf(),
199        _ => PathBuf::from("."),
200    };
201    if root.is_absolute() {
202        return Ok(root);
203    }
204    let cwd = std::env::current_dir().map_err(|error| {
205        PrayError::Resolution(format!("failed to resolve project root from cwd: {error}"))
206    })?;
207    Ok(cwd.join(root))
208}