1#![allow(clippy::result_large_err)]23use std::{collections::HashSet, fs, path::Path};45use gix::{6 bstr::{self, ByteSlice},7 interrupt, progress,8 remote::{self, ref_map},9};10use tracing::info;1112use super::{Error, LocalExtraction, ResolveResult, Result, VendorSource, cache_dir};13use crate::jsonnet_bundler::{Dependency, GitSource, JsonnetFile, Source, SubDir};1415fn repo_cache_path(remote: &GitSource) -> Result<std::path::PathBuf> {16 Ok(cache_dir("git")?.join(&remote.host).join(&remote.repo))17}1819fn ensure_repo(remote: &GitSource) -> Result<gix::Repository> {20 let cache_path = repo_cache_path(remote)?;2122 if cache_path.exists() {23 if let Ok(repo) = gix::open(&cache_path) {24 fetch_remote(&repo, &remote.remote())?;25 return Ok(repo);26 }27 fs::remove_dir_all(&cache_path).map_err(|e| Error::Io(cache_path.clone(), e))?;28 }2930 fs::create_dir_all(cache_path.parent().expect("has parent"))31 .map_err(|e| Error::Io(cache_path.clone(), e))?;3233 let mut clone = gix::prepare_clone_bare(remote.remote(), &cache_path)?;34 let (repo, _) = clone.fetch_only(progress::Discard, &interrupt::IS_INTERRUPTED)?;35 fetch_remote(&repo, &remote.remote())?;3637 Ok(repo)38}3940fn fetch_remote(repo: &gix::Repository, remote: &str) -> Result<(), Error> {41 repo.remote_at(remote)?42 .with_refspecs(["+refs/*:refs/*"], remote::Direction::Fetch)?43 .connect(remote::Direction::Fetch)?44 .prepare_fetch(progress::Discard, ref_map::Options::default())?45 .receive(progress::Discard, &interrupt::IS_INTERRUPTED)?;46 Ok(())47}4849fn extract_tree(50 repo: &gix::Repository,51 tree: &gix::Tree<'_>,52 subdir: &SubDir,53 dest: &Path,54) -> Result<(), Error> {55 let target_tree;56 let tree = if subdir.is_empty() {57 tree58 } else {59 let mut t = tree.clone();60 let entry = t61 .peel_to_entry_by_path(subdir.as_path().as_std_path())?62 .ok_or_else(|| Error::SubdirNotFound(subdir.to_string()))?;63 target_tree = entry.object()?.into_tree();64 &target_tree65 };6667 let files = tree.traverse().breadthfirst.files()?;6869 for entry in &files {70 if !entry.mode.is_blob() {71 continue;72 }73 let rel_path = entry74 .filepath75 .to_str()76 .map_err(|_| Error::InvalidPath(entry.filepath.to_string()))?;77 let file_path = dest.join(rel_path);7879 if let Some(parent) = file_path.parent() {80 fs::create_dir_all(parent).map_err(|e| Error::Io(parent.to_owned(), e))?;81 }8283 let blob = repo.find_object(entry.oid)?;84 fs::write(&file_path, &blob.data).map_err(|e| Error::Io(file_path, e))?;85 }8687 Ok(())88}8990fn resolve_version<'r>(repo: &'r gix::Repository, version: &str) -> Result<gix::Id<'r>> {91 let spec: &bstr::BStr = version.into();92 if let Ok(id) = repo.rev_parse_single(spec) {93 return Ok(id);94 }95 for prefix in ["refs/heads/", "refs/tags/"] {96 let refname = format!("{prefix}{version}");97 if let Ok(r) = repo.find_reference(&refname) {98 return Ok(r.into_fully_peeled_id()?);99 }100 }101 Ok(repo.rev_parse_single(spec)?)102}103104fn read_blob_at_path(105 repo: &gix::Repository,106 tree: &gix::Tree<'_>,107 path: &SubDir,108) -> Option<Vec<u8>> {109 let mut t = tree.clone();110 let entry = t111 .peel_to_entry_by_path(path.as_path().as_std_path())112 .ok()??;113 let blob = repo.find_object(entry.oid()).ok()?;114 Some(blob.data.clone())115}116117fn collect_tree_deps(118 repo: &gix::Repository,119 tree: &gix::Tree<'_>,120 dir: &SubDir,121 git_deps: &mut Vec<Dependency>,122 local_extractions: &mut Vec<LocalExtraction>,123 visited: &mut HashSet<SubDir>,124) {125 if !visited.insert(dir.clone()) {126 return;127 }128129 let manifest_path = dir130 .join("jsonnetfile.json")131 .expect("appending a literal filename keeps it within parent");132 let Some(data) = read_blob_at_path(repo, tree, &manifest_path) else {133 return;134 };135 let Ok(manifest) = serde_json::from_slice::<JsonnetFile>(&data) else {136 return;137 };138139 for dep in manifest.dependencies {140 match &dep.source {141 Source::Git(_) => git_deps.push(dep),142 Source::Local(local) => {143 let Ok(child_dir) = local.resolve_under(dir) else {144 info!("local source {local} escapes its package; skipping");145 continue;146 };147 let name = child_dir148 .file_name()149 .map_or_else(|| local.to_string(), str::to_owned);150 local_extractions.push(LocalExtraction {151 tree_path: child_dir.clone(),152 name,153 });154 collect_tree_deps(repo, tree, &child_dir, git_deps, local_extractions, visited);155 }156 }157 }158}159160pub(super) fn resolve(161 git_source: &GitSource,162 version: Option<&str>,163) -> Result<ResolveResult, Error> {164 info!("fetching via git: {}", git_source.remote());165 let repo = ensure_repo(git_source)?;166 let id = match version {167 Some(v) => resolve_version(&repo, v)?,168 None => repo.head_id()?,169 };170 let commit = repo.find_object(id)?.peel_to_commit()?;171 let tree = commit.tree()?;172173 let mut transitive_git_deps = Vec::new();174 let mut local_extractions = Vec::new();175 let mut visited = HashSet::new();176 collect_tree_deps(177 &repo,178 &tree,179 &git_source.subdir,180 &mut transitive_git_deps,181 &mut local_extractions,182 &mut visited,183 );184185 let repo_path = repo_cache_path(git_source)?;186 let sha = commit.id.to_string();187188 Ok(ResolveResult {189 version: sha.clone(),190 transitive_git_deps,191 local_extractions,192 source: VendorSource::GitTree {193 repo_path,194 commit_sha: sha,195 subdir: git_source.subdir.clone(),196 },197 })198}199200pub(super) fn extract(201 repo_path: &Path,202 commit_sha: &str,203 subdir: &SubDir,204 dest: &Path,205) -> Result<(), Error> {206 let repo = gix::open(repo_path)?;207 let spec: &bstr::BStr = commit_sha.into();208 let id = repo.rev_parse_single(spec)?;209 let commit = repo.find_object(id)?.peel_to_commit()?;210 let tree = commit.tree()?;211 extract_tree(&repo, &tree, subdir, dest)212}