git.delta.rocks / jrsonnet / refs/commits / de9f0cb0f75a

difftreelog

source

crates/jrsonnet-pkg/src/install/git.rs5.4 KiBsourcehistory
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}