pub mod dependency;
pub mod git;
pub mod lockfile;
pub mod resolver;
use crate::git::GitPackage;
use crate::git::PackageUrl;
use anyhow::Context;
use lockfile::LockFile;
use once_cell::sync::Lazy;
use prodash::tree::Root;
use pubgrub::Ranges;
use pubgrub::resolve;
use rayon::prelude::*;
use resolver::GitDependencyProvider;
use semver::Version;
use std::collections::BTreeMap;
use std::env;
use std::fs;
use std::iter::zip;
use std::ops::RangeInclusive;
use std::path::Path;
pub mod third_party;
static PROGRESS: Lazy<std::sync::Arc<Root>> = Lazy::new(|| {
prodash::tree::root::Options {
message_buffer_capacity: 200,
..Default::default()
}
.into()
});
const DEFAULT_FRAME_RATE: f32 = 6.0;
fn setup_line_renderer_range(
progress: &std::sync::Arc<prodash::tree::Root>,
levels: std::ops::RangeInclusive<prodash::progress::key::Level>,
) -> prodash::render::line::JoinHandle {
prodash::render::line(
std::io::stderr(),
std::sync::Arc::downgrade(progress),
prodash::render::line::Options {
level_filter: Some(levels),
frames_per_second: DEFAULT_FRAME_RATE,
initial_delay: Some(std::time::Duration::from_millis(1000)),
timestamp: true,
throughput: true,
hide_cursor: true,
..prodash::render::line::Options::default()
}
.auto_configure(prodash::render::line::StreamKind::Stderr),
)
}
pub static VERBOSE: Lazy<bool> = Lazy::new(|| {
env::args().any(|arg| arg == "-v" || arg == "--verbose")
});
pub fn sync_dependencies() -> anyhow::Result<()> {
let lockfile = LockFile::from_file("gipm.lock")?;
let packages: Vec<(String, lockfile::LockFilePackage)> =
lockfile.packages.into_iter().collect();
let progress_range: RangeInclusive<u8> = 1..=4;
let handle = setup_line_renderer_range(&PROGRESS, progress_range);
let checkout_results: Vec<(&str, &str, GitPackage, anyhow::Result<()>)> = packages
.par_iter()
.map(
|(name, package)| -> (&str, &str, GitPackage, anyhow::Result<()>) {
let mut dep = GitPackage::new(
PackageUrl::GitUrl(package.source.clone()),
Some(name.clone()),
None,
);
let object = match package.commit.parse::<gix::ObjectId>() {
Ok(object) => object,
Err(e) => {
return (
name,
&package.commit,
dep,
Err(anyhow::anyhow!(format!(
"Invalid commit hash {}: {e:?}",
package.commit
))),
);
}
};
let should_update = dep.does_id_exist_in_db(object).unwrap_or(false);
if should_update && let Err(e) = dep.update_db() {
return (
name,
&package.commit,
dep,
Err(e.context(format!(
"Failed to update database for {} at commit {}",
name, package.commit
))),
);
};
if *VERBOSE {
println!(
"Updating checkout for {} at commit {}",
name, package.commit
);
}
let mut sub_progress = PROGRESS.add_child(format!(
"Updating checkout for {} at commit {}",
name, package.commit
));
match dep.checkout_or_clone_object_from_database(object, &mut sub_progress) {
Ok(_) => {
sub_progress.done("Complete");
(name, &package.commit, dep, Ok(()))
}
Err(e) => {
sub_progress.fail(format!("Failed:\n\t{e:?}"));
(name, &package.commit, dep, Err(e))
}
}
},
)
.collect();
handle.shutdown_and_wait();
let mut sync_success = true;
println!("Synced dependencies:");
for (name, version, dep, result) in checkout_results {
match result {
Ok(_) => {
if dep.is_dirty() {
println!(" ❗ {}: {version} - dirty workspace", dep.name());
} else {
println!(" ✅ {}: {version}", dep.name());
}
}
Err(e) => {
eprintln!("❌ Failed to check out {name} at version {version}: {e:?}");
sync_success = false;
}
}
}
match sync_success {
true => {
println!("✅ All dependencies synced successfully!");
Ok(())
}
false => Err(anyhow::anyhow!(
"❌ Some dependencies failed to sync. See above log for details."
)),
}
}
pub fn clean() -> anyhow::Result<()> {
let git_deps_dir = Path::new(".gitvenv");
if git_deps_dir.exists() {
fs::remove_dir_all(git_deps_dir)?;
println!("Deleted .gitvenv directory");
}
Ok(())
}
pub fn install_dependencies() -> anyhow::Result<()> {
let progress_range: RangeInclusive<u8> = 1..=4;
let handle = setup_line_renderer_range(&PROGRESS, progress_range);
let mut resolver = GitDependencyProvider::default();
let root_dep = GitPackage::new(git::PackageUrl::Root, None, None);
let dummy_root_version = Version::new(0, 0, 0);
let dependency_specs = dependency::parse_dependencies_yaml_file("dependencies.yaml")?;
let resolver_dependencies: Vec<(GitPackage, Ranges<Version>)> =
dependency::dependency_spec_to_package_and_version_range(dependency_specs)
.context("Failed to parse dependency specification for root project")?;
resolver.add_dependencies(
root_dep.clone(),
dummy_root_version.clone(),
resolver_dependencies,
);
let mut resolved_dependencies = resolve(&resolver, root_dep.clone(), dummy_root_version)?;
resolved_dependencies.remove(&root_dep);
let result: Vec<anyhow::Result<()>> = resolved_dependencies
.par_iter()
.map(|(dep, version)| {
if *VERBOSE {
println!("Checking out {} at version {version}", dep.name());
}
dep.checkout_from_database(version)
})
.collect();
handle.shutdown_and_wait();
for (result, (dep, ver)) in zip(&result, &resolved_dependencies) {
if let Err(e) = result {
anyhow::bail!("Error checking out - {} version {ver}: {e}", dep.name())
}
}
println!("Resolved dependencies:");
for (dep, version) in &resolved_dependencies {
if let PackageUrl::GitUrl(_) = &dep.url {
if dep.is_dirty() {
println!(
" ❗ {}: {version} (tag {}) - dirty workspace",
dep.name(),
dep.get_tag_name(version).unwrap_or("invalid".to_string())
);
} else {
println!(
" ✅ {}: {version} (tag {})",
dep.name(),
dep.get_tag_name(version).unwrap_or("invalid".to_string())
);
}
}
}
let mut lockfile = LockFile::default();
for (dep, version) in &resolved_dependencies {
if dep.name() != root_dep.clone().name() {
let repo = gix::open(dep.get_checkout_path()?)?;
let commit = repo.head_commit().expect("HEAD should exist");
let package_dependencies = dep.fetch_dependencies_yaml(version)?;
let mut deps_map = BTreeMap::new();
if let Some(deps) = package_dependencies {
for dep_spec in deps {
let req = dep.parse_version_requirement(
&dep_spec.version,
&dep.prefix,
&dep.replacements,
)?;
deps_map.insert(git::normalize_url(&PackageUrl::GitUrl(dep_spec.url)), req);
}
}
let dependencies_to_add = if deps_map.is_empty() {
None
} else {
Some(deps_map)
};
lockfile.add_package(
dep.name(),
version.clone(),
dep.url.as_str().to_string(),
commit.id.to_string(),
dependencies_to_add,
)?;
}
}
lockfile.to_file("gipm.lock")?;
println!("✅ All dependencies resolved and checked out successfully!");
println!("🔒 Lockfile updated at gipm.lock");
Ok(())
}