use crate::{MaterializeError, Vcs};
use atomicwrites::AtomicFile;
use camino::{Utf8Component, Utf8Path, Utf8PathBuf};
use fs_err as fs;
use git_stub::GitStub;
use std::io::Write;
fn find_non_normal_component(path: &Utf8Path) -> Option<String> {
path.components().find_map(|component| match component {
Utf8Component::Normal(_) => None,
Utf8Component::Prefix(_)
| Utf8Component::RootDir
| Utf8Component::CurDir
| Utf8Component::ParentDir => Some(component.as_str().to_owned()),
})
}
fn check_path(path: &Utf8Path) -> Result<(), MaterializeError> {
if let Some(component) = find_non_normal_component(path) {
return Err(MaterializeError::InvalidPathComponent {
path: path.to_owned(),
component,
});
}
Ok(())
}
#[derive(Debug, Clone)]
pub struct Materializer {
repo_root: Utf8PathBuf,
output_dir: Utf8PathBuf,
emit_cargo_directives: bool,
vcs: Vcs,
}
impl Materializer {
pub fn standard(
repo_root: impl Into<Utf8PathBuf>,
output_dir: impl Into<Utf8PathBuf>,
) -> Result<Self, MaterializeError> {
let repo_root = repo_root.into();
let vcs = Vcs::detect(&repo_root)?;
Self::check_shallow(&vcs, &repo_root)?;
Ok(Materializer {
repo_root,
output_dir: output_dir.into(),
emit_cargo_directives: false,
vcs,
})
}
pub fn for_build_script(
repo_root: impl Into<Utf8PathBuf>,
) -> Result<Self, MaterializeError> {
let out_dir = std::env::var("OUT_DIR").expect(
"OUT_DIR is set \
(must be called from a Cargo build script)",
);
let out_dir = Utf8PathBuf::from(out_dir).join("git-stub-vcs");
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect(
"CARGO_MANIFEST_DIR is set \
(must be called from a Cargo build script)",
);
let manifest_dir = Utf8PathBuf::from(manifest_dir);
let repo_root = manifest_dir.join(repo_root.into());
let vcs = Vcs::detect(&repo_root)?;
Self::check_shallow(&vcs, &repo_root)?;
Ok(Materializer {
repo_root,
output_dir: out_dir,
emit_cargo_directives: true,
vcs,
})
}
pub fn with_vcs(mut self, vcs: Vcs) -> Result<Self, MaterializeError> {
Self::check_shallow(&vcs, &self.repo_root)?;
self.vcs = vcs;
Ok(self)
}
pub fn vcs(&self) -> &Vcs {
&self.vcs
}
fn check_shallow(
vcs: &Vcs,
repo_root: &Utf8Path,
) -> Result<(), MaterializeError> {
if vcs.is_shallow_clone(repo_root).map_err(|error| {
MaterializeError::ShallowCloneCheck {
repo_root: repo_root.to_owned(),
error,
}
})? {
return Err(MaterializeError::ShallowClone {
vcs: vcs.name(),
repo_root: repo_root.to_owned(),
});
}
Ok(())
}
pub fn materialize(
&self,
git_stub_path: impl AsRef<Utf8Path>,
) -> Result<Utf8PathBuf, MaterializeError> {
let git_stub_path = git_stub_path.as_ref();
check_path(git_stub_path)?;
if git_stub_path.extension() != Some("gitstub") {
return Err(MaterializeError::NotGitStub {
path: git_stub_path.to_owned(),
});
}
let output_path =
self.output_dir.join(git_stub_path.with_extension(""));
self.materialize_inner(git_stub_path, &output_path)?;
Ok(output_path)
}
pub fn materialize_to(
&self,
git_stub_path: impl AsRef<Utf8Path>,
output_path: impl AsRef<Utf8Path>,
) -> Result<(), MaterializeError> {
let git_stub_path = git_stub_path.as_ref();
let output_path = output_path.as_ref();
check_path(git_stub_path)?;
if git_stub_path.extension() != Some("gitstub") {
return Err(MaterializeError::NotGitStub {
path: git_stub_path.to_owned(),
});
}
let output_path = self.output_dir.join(output_path);
self.materialize_inner(git_stub_path, &output_path)
}
fn materialize_inner(
&self,
git_stub_path: &Utf8Path,
output_path: &Utf8Path,
) -> Result<(), MaterializeError> {
let full_git_stub_path = self.repo_root.join(git_stub_path);
if self.emit_cargo_directives {
println!("cargo::rerun-if-changed={}", full_git_stub_path);
}
let git_stub_contents = fs::read_to_string(&full_git_stub_path)
.map_err(|error| MaterializeError::ReadGitStub {
path: full_git_stub_path.clone(),
error,
})?;
let git_stub: GitStub = git_stub_contents.parse().map_err(|error| {
MaterializeError::InvalidGitStub { path: full_git_stub_path, error }
})?;
let content =
self.vcs.read_git_stub_contents(&git_stub, &self.repo_root)?;
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent).map_err(|error| {
MaterializeError::CreateDir { path: parent.to_owned(), error }
})?;
}
AtomicFile::new(
output_path,
atomicwrites::OverwriteBehavior::AllowOverwrite,
)
.write(|f| f.write_all(&content))
.map_err(|error| {
use crate::errors::AtomicWriteError;
let error = match error {
atomicwrites::Error::Internal(e) => AtomicWriteError::Rename(e),
atomicwrites::Error::User(e) => AtomicWriteError::Write(e),
};
MaterializeError::WriteOutput {
path: output_path.to_owned(),
error,
}
})?;
Ok(())
}
}