use crate::{util, PathBuf};
use anyhow::{Context as _, Result};
pub struct GitPackage {
pub db: bytes::Bytes,
pub checkout: Option<bytes::Bytes>,
}
const DIR: gix::remote::Direction = gix::remote::Direction::Fetch;
use gix::progress::Discard;
#[tracing::instrument(level = "debug")]
pub fn clone(src: &crate::cargo::GitSource) -> Result<GitPackage> {
let temp_dir = tempfile::tempdir()?;
let submodule_dir = tempfile::tempdir()?;
let (repo, _out) = {
let span = tracing::debug_span!("fetch");
let _fs = span.enter();
gix::prepare_clone_bare(src.url.as_str(), temp_dir.path())
.context("failed to prepare clone")?
.with_remote_name("origin")?
.configure_remote(|remote| {
Ok(remote
.with_fetch_tags(gix::remote::fetch::Tags::All)
.with_refspecs(["+HEAD:refs/remotes/origin/HEAD"], DIR)?)
})
.fetch_only(&mut Discard, &Default::default())
.context("failed to fetch")?
};
repo.find_object(src.rev.id).with_context(|| {
format!(
"'{}' doesn't contain rev '{}'",
src.url,
src.rev.id.to_hex()
)
})?;
let fetch_rev = src.rev.id;
let temp_db_path = util::path(temp_dir.path())?;
let sub_dir_path = util::path(submodule_dir.path())?;
let (checkout, db) = rayon::join(
|| -> anyhow::Result<_> {
let span = tracing::info_span!("cloning submodules", %src.url);
let _ms = span.enter();
crate::git::prepare_submodules(
temp_db_path.to_owned(),
sub_dir_path.to_owned(),
fetch_rev,
)?;
util::pack_tar(sub_dir_path)
},
|| -> anyhow::Result<_> { util::pack_tar(temp_db_path) },
);
Ok(crate::git::GitPackage {
db: db?,
checkout: match checkout {
Ok(co) => Some(co),
Err(err) => {
tracing::error!("failed to checkout: {err:#}");
None
}
},
})
}
#[tracing::instrument(level = "debug")]
pub(crate) fn checkout(
src: PathBuf,
target: PathBuf,
rev: gix::ObjectId,
) -> Result<gix::Repository> {
std::fs::create_dir_all(target.parent().unwrap()).context("failed to create checkout dir")?;
if target.exists() {
remove_dir_all::remove_dir_all(&target).context("failed to clean checkout dir")?;
}
{
let start = std::time::Instant::now();
let mut cmd = std::process::Command::new("git");
cmd.args(["clone", "--local", "--no-checkout"])
.args([&src, &target])
.stderr(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped());
let output = cmd.output().context("failed to spawn git")?;
if !output.status.success() {
let error = String::from_utf8(output.stderr)
.unwrap_or_else(|_err| "git error output is non-utf8".to_owned());
anyhow::bail!("failed to perform local clone:\n{error}");
}
tracing::debug!("local clone performed in {}ms", start.elapsed().as_millis());
}
let mut repo = gix::open(target).context("failed to open local clone")?;
modify_config(&mut repo, |config| {
let mut core = config
.section_mut("core", None)
.context("unable to find core section")?;
core.set(
"autocrlf"
.try_into()
.context("autocrlf is not a valid key")?,
"false",
);
Ok(())
})
.context("failed to set autocrlf")?;
reset(&mut repo, rev)?;
Ok(repo)
}
use gix::bstr::BString;
use gix::bstr::ByteSlice;
struct Submodule {
name: BString,
path: BString,
url: BString,
branch: Option<BString>,
head_id: Option<gix::ObjectId>,
}
impl Submodule {
#[inline]
fn path(&self) -> &crate::Path {
crate::Path::new(self.path.to_str().unwrap())
}
}
fn read_submodule_config(config: &gix::config::File<'_>) -> Vec<Submodule> {
let Some(iter) = config.sections_by_name("submodule") else { return Vec::new(); };
iter.filter_map(|sec| {
let name = sec.header().subsection_name()?;
let url = sec.value("url")?;
let path = sec.value("path")?;
path.to_str().ok()?;
let branch = sec.value("branch");
Some(Submodule {
name: name.into(),
url: url.into_owned(),
path: path.into_owned(),
branch: branch.map(|b| b.into_owned()),
head_id: None,
})
})
.collect()
}
fn modify_config(
repo: &mut gix::Repository,
mutate: impl FnOnce(&mut gix::config::SnapshotMut<'_>) -> Result<()>,
) -> Result<()> {
let mut config = repo.config_snapshot_mut();
mutate(&mut config)?;
{
use std::io::Write;
let mut local_config = std::fs::OpenOptions::new()
.create(false)
.write(true)
.append(false)
.open(
config
.meta()
.path
.as_deref()
.context("local config with path set")?,
)
.context("failed to open local config")?;
local_config.write_all(config.detect_newline_style())?;
config
.write_to_filter(&mut local_config, |s| {
s.meta().source == gix::config::Source::Local
})
.context("failed to write submodules to config")?;
}
config
.commit()
.context("failed to commit submodule(s) to config")?;
Ok(())
}
fn reset(repo: &mut gix::Repository, rev: gix::ObjectId) -> Result<()> {
let workdir = repo
.work_dir()
.context("unable to checkout, repository is bare")?;
let root_tree = repo
.find_object(rev)
.context("failed to find revision")?
.peel_to_tree()
.context("unable to peel to tree")?
.id;
use gix::odb::FindExt;
let index = gix::index::State::from_tree(&root_tree, |oid, buf| {
repo.objects.find_tree_iter(oid, buf).ok()
})
.with_context(|| format!("failed to create index from tree '{root_tree}'"))?;
let mut index = gix::index::File::from_state(index, repo.index_path());
let opts = gix::worktree::checkout::Options {
destination_is_initially_empty: false,
overwrite_existing: true,
..Default::default()
};
gix::worktree::checkout(
&mut index,
workdir,
{
let objects = repo.objects.clone().into_arc()?;
move |oid, buf| objects.find_blob(oid, buf)
},
&mut Discard,
&mut Discard,
&Default::default(),
opts,
)
.context("failed to checkout")?;
index
.write(Default::default())
.context("failed to write index")?;
let head_path = repo.path().join("HEAD");
std::fs::write(head_path, format!("{}\n", rev.to_hex()))?;
Ok(())
}
#[tracing::instrument(level = "debug")]
pub(crate) fn prepare_submodules(src: PathBuf, target: PathBuf, rev: gix::ObjectId) -> Result<()> {
fn update_submodules(repo: &mut gix::Repository, rev: gix::ObjectId) -> Result<()> {
let work_dir = repo.work_dir().unwrap().to_owned();
let submodules_config = work_dir.join(".gitmodules");
if !submodules_config.exists() {
return Ok(());
}
let subm_config_buf =
std::fs::read(&submodules_config).context("failed to read .gitmodules")?;
let submodules_file = {
let meta = gix::config::file::Metadata {
path: Some(submodules_config.clone()),
source: gix::config::Source::Local,
level: 0,
trust: gix::sec::Trust::Full,
};
gix::config::File::from_bytes_no_includes(&subm_config_buf, meta, Default::default())
.context("failed to deserialize .gitmodules")?
};
let submodules = {
let mut submodules = read_submodule_config(&submodules_file);
if submodules.is_empty() {
tracing::info!("repo contained a .gitmodules file, but it had no valid submodules");
return Ok(());
}
modify_config(repo, |config| {
for subm in &submodules {
if config
.section("submodule", Some(subm.name.as_bstr()))
.is_ok()
{
tracing::debug!("submodule {} already exists in config", subm.name);
continue;
}
let mut sec = config
.new_section("submodule", Some(subm.name.clone().into()))
.context("failed to add submodule section")?;
sec.push("path".try_into()?, Some(subm.path.as_bstr()));
sec.push("url".try_into()?, Some(subm.url.as_bstr()));
if let Some(branch) = &subm.branch {
sec.push("branch".try_into()?, Some(branch.as_bstr()));
}
}
Ok(())
})
.context("failed to add submodules")?;
let tree = repo
.find_object(rev)
.context("failed to find rev")?
.peel_to_tree()
.context("failed to peel rev to tree")?;
let mut buf = Vec::new();
for subm in &mut submodules {
let span = tracing::info_span!("locating submodule head", name = %subm.name, path = %subm.path);
let _ms = span.enter();
let path = subm.path();
let entry = match tree.lookup_entry(path, &mut buf) {
Ok(Some(e)) => e,
Ok(None) => {
tracing::warn!("unable to locate submodule path in tree");
continue;
}
Err(err) => {
tracing::warn!(err = %err, "failed to lookup entry for submodule");
continue;
}
};
if !matches!(entry.mode(), gix::object::tree::EntryMode::Commit) {
tracing::warn!(kind = ?entry.mode(), "path is not a submodule");
continue;
}
subm.head_id = Some(entry.id().detach());
}
submodules
};
use rayon::prelude::*;
let mut res = Vec::new();
submodules
.into_par_iter()
.map(|subm| update_submodule(&work_dir, subm).context("failed to update submodule"))
.collect_into_vec(&mut res);
res.into_iter().collect::<Result<()>>()?;
Ok(())
}
#[tracing::instrument(level = "debug", skip_all)]
fn update_submodule(parent: &std::path::Path, subm: Submodule) -> Result<()> {
let Some(head) = subm.head_id else {
tracing::debug!(
"skipping submodule '{}' without HEAD",
subm.name
);
return Ok(());
};
let submodule_path = parent.join(subm.path());
let open_or_init_repo = || -> Result<_> {
let open_with_complete_config =
gix::open::Options::default().permissions(gix::open::Permissions {
config: gix::open::permissions::Config {
git_binary: true,
..Default::default()
},
..Default::default()
});
let repo = if let Ok(repo) = gix::open_opts(&submodule_path, open_with_complete_config)
{
repo
} else {
let _ = remove_dir_all::remove_dir_all(&submodule_path);
gix::init(&submodule_path).context("failed to init submodule")?
};
Ok(repo)
};
let mut repo = open_or_init_repo()?;
if repo
.head_commit()
.ok()
.map_or(false, |commit| commit.id == head)
{
return update_submodules(&mut repo, head);
}
modify_config(&mut repo, |config| {
let mut core = config
.section_mut("core", None)
.context("unable to find core section")?;
core.set(
"autocrlf"
.try_into()
.context("autocrlf is not a valid key")?,
"false",
);
config
.set_raw_value("committer", None, "name", "cargo-fetcher")
.context("failed to set committer.name")?;
config
.set_raw_value("committer", None, "email", "")
.context("failed to set committer.email")?;
Ok(())
})?;
let mut remote = repo
.remote_at(subm.url.as_bstr())
.context("invalid submodule url")?;
remote
.replace_refspecs(
[
"+refs/heads/*:refs/remotes/origin/*",
"+HEAD:refs/remotes/origin/HEAD",
],
DIR,
)
.expect("valid statically known refspec");
remote = remote.with_fetch_tags(gix::remote::fetch::Tags::All);
let outcome = remote
.connect(DIR)
.context("failed to connect to remote")?
.prepare_fetch(&mut Discard, Default::default())
.context("failed to prepare fetch")?
.receive(&mut Discard, &Default::default())
.context("failed to fetch submodule")?;
tame_index::utils::git::write_fetch_head(&repo, &outcome, &remote)
.context("failed to write FETCH_HEAD")?;
reset(&mut repo, head)?;
update_submodules(&mut repo, head)
}
let mut repo = checkout(src, target, rev)?;
update_submodules(&mut repo, rev)
}