use git2::{self, Error as GitError, Repository, Tree, Oid};
use semver::{VersionReq as SemverReq, Version as Semver};
use std::fs::{self, DirEntry, File};
use std::path::{PathBuf, Path};
use std::time::SystemTime;
use std::io::Read;
use regex::Regex;
use url::Url;
use std::cmp;
use toml;
use json;
mod config;
pub use self::config::*;
lazy_static! {
static ref MAIN_PACKAGE_RGX: Regex = Regex::new(r"([^\s]+) ([^\s]+) \(registry+\+([^\s]+)\)").unwrap();
static ref GIT_PACKAGE_RGX: Regex = Regex::new(r"([^\s]+) ([^\s]+) \(git+\+([^#\s]+)#([^\s]{40})\)").unwrap();
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct MainRepoPackage {
pub name: String,
pub version: Option<Semver>,
pub newest_version: Option<Semver>,
pub max_version: Option<Semver>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct GitRepoPackage {
pub name: String,
pub url: String,
pub branch: Option<String>,
pub id: Oid,
pub newest_id: Option<Oid>,
}
impl MainRepoPackage {
pub fn parse(what: &str) -> Option<MainRepoPackage> {
MAIN_PACKAGE_RGX.captures(what).map(|c| {
MainRepoPackage {
name: c.get(1).unwrap().as_str().to_string(),
version: Some(Semver::parse(c.get(2).unwrap().as_str()).unwrap()),
newest_version: None,
max_version: None,
}
})
}
pub fn pull_version<'t>(&mut self, registry: &Tree<'t>, registry_parent: &'t Repository) {
let vers = crate_versions(&mut &find_package_data(&self.name, registry, registry_parent).unwrap()[..]);
self.newest_version = vers.into_iter().max();
}
pub fn needs_update(&self, req: Option<&SemverReq>) -> bool {
(req.into_iter().zip(self.version.as_ref()).map(|(sr, cv)| !sr.matches(cv)).next().unwrap_or(true) ||
req.into_iter().zip(self.update_to_version()).map(|(sr, uv)| sr.matches(uv)).next().unwrap_or(true)) &&
self.update_to_version().map(|upd_v| self.version.is_none() || (*self.version.as_ref().unwrap() < *upd_v)).unwrap_or(false)
}
pub fn update_to_version(&self) -> Option<&Semver> {
self.newest_version.as_ref().map(|new_v| cmp::min(new_v, self.max_version.as_ref().unwrap_or(new_v)))
}
}
impl GitRepoPackage {
pub fn parse(what: &str) -> Option<GitRepoPackage> {
GIT_PACKAGE_RGX.captures(what).map(|c| {
let mut url = Url::parse(c.get(3).unwrap().as_str()).unwrap();
let branch = url.query_pairs().find(|&(ref name, _)| name == "branch").map(|(_, value)| value.to_string());
url.set_query(None);
GitRepoPackage {
name: c.get(1).unwrap().as_str().to_string(),
url: url.into_string(),
branch: branch,
id: Oid::from_str(c.get(4).unwrap().as_str()).unwrap(),
newest_id: None,
}
})
}
pub fn pull_version<P: AsRef<Path>>(&mut self, temp_dir: P) {
fs::create_dir_all(temp_dir.as_ref()).unwrap();
let clone_dir = temp_dir.as_ref().join(&self.name);
let repo = if clone_dir.exists() {
let mut r = git2::Repository::open(clone_dir);
if let Ok(ref mut r) = r.as_mut() {
r.find_remote("origin").and_then(|mut rm| rm.fetch(&[self.branch.as_ref().map(String::as_str).unwrap_or("master")], None, None)).unwrap();
r.set_head("FETCH_HEAD").unwrap();
}
r
} else {
let mut bldr = git2::build::RepoBuilder::new();
bldr.bare(true);
if let Some(ref b) = self.branch.as_ref() {
bldr.branch(b);
}
bldr.clone(&self.url, &clone_dir)
};
self.newest_id = Some(repo.and_then(|r| r.head().and_then(|h| h.target().ok_or_else(|| GitError::from_str("HEAD not a direct reference")))).unwrap());
}
pub fn needs_update(&self) -> bool {
self.newest_id.is_some() && self.id != *self.newest_id.as_ref().unwrap()
}
}
pub fn resolve_crates_file(crates_file: PathBuf) -> PathBuf {
let config_file = crates_file.with_file_name("config");
if config_file.exists() {
let mut crates = String::new();
File::open(&config_file).unwrap().read_to_string(&mut crates).unwrap();
if let Some(idir) = toml::from_str::<toml::Value>(&crates)
.unwrap()
.get("install")
.and_then(|t| t.as_table())
.and_then(|t| t.get("root"))
.and_then(|t| t.as_str()) {
return resolve_crates_file(Path::new(idir).join(".crates.toml"));
}
}
crates_file
}
pub fn installed_main_repo_packages(crates_file: &Path) -> Vec<MainRepoPackage> {
if crates_file.exists() {
let mut crates = String::new();
File::open(crates_file).unwrap().read_to_string(&mut crates).unwrap();
let mut res = Vec::<MainRepoPackage>::new();
for pkg in toml::from_str::<toml::Value>(&crates).unwrap()["v1"].as_table().unwrap().keys().flat_map(|s| MainRepoPackage::parse(s)) {
if let Some(saved) = res.iter_mut().find(|p| p.name == pkg.name) {
if saved.version.is_none() || saved.version.as_ref().unwrap() < pkg.version.as_ref().unwrap() {
saved.version = pkg.version;
}
continue;
}
res.push(pkg);
}
res
} else {
Vec::new()
}
}
pub fn installed_git_repo_packages(crates_file: &Path) -> Vec<GitRepoPackage> {
if crates_file.exists() {
let mut crates = String::new();
File::open(crates_file).unwrap().read_to_string(&mut crates).unwrap();
let mut res = Vec::<GitRepoPackage>::new();
for pkg in toml::from_str::<toml::Value>(&crates).unwrap()["v1"].as_table().unwrap().keys().flat_map(|s| GitRepoPackage::parse(s)) {
if let Some(saved) = res.iter_mut().find(|p| p.name == pkg.name) {
saved.id = pkg.id;
continue;
}
res.push(pkg);
}
res
} else {
Vec::new()
}
}
pub fn intersect_packages(installed: &[MainRepoPackage], to_update: &[(String, Option<Semver>)], allow_installs: bool) -> Vec<MainRepoPackage> {
installed.iter()
.filter(|p| to_update.iter().any(|u| p.name == u.0))
.cloned()
.map(|p| MainRepoPackage { max_version: to_update.iter().find(|u| p.name == u.0).and_then(|u| u.1.clone()), ..p })
.chain(to_update.iter().filter(|p| allow_installs && installed.iter().find(|i| i.name == p.0).is_none()).map(|p| {
MainRepoPackage {
name: p.0.clone(),
version: None,
newest_version: None,
max_version: p.1.clone(),
}
}))
.collect()
}
pub fn crate_versions<R: Read>(package_desc: &mut R) -> Vec<Semver> {
let mut buf = String::new();
package_desc.read_to_string(&mut buf).unwrap();
buf.lines()
.map(|p| json::parse(p).unwrap())
.filter(|j| !j["yanked"].as_bool().unwrap())
.map(|j| Semver::parse(j["vers"].as_str().unwrap()).unwrap())
.collect()
}
pub fn get_index_path(cargo_dir: &Path) -> PathBuf {
fs::read_dir(cargo_dir.join("registry").join("index"))
.unwrap()
.map(Result::unwrap)
.filter(|i| i.file_type().unwrap().is_dir())
.max_by_key(latest_modified)
.unwrap()
.path()
}
fn latest_modified(ent: &DirEntry) -> SystemTime {
let meta = ent.metadata().unwrap();
let mut latest = meta.modified().unwrap();
if meta.is_dir() {
for ent in fs::read_dir(ent.path()).unwrap() {
latest = cmp::max(latest, latest_modified(&ent.unwrap()));
}
}
latest
}
pub fn find_package_data<'t>(cratename: &str, registry: &Tree<'t>, registry_parent: &'t Repository) -> Option<Vec<u8>> {
macro_rules! try_opt {
($expr:expr) => {
match $expr {
Some(e) => e,
None => return None,
}
}
}
let clen = cratename.len().to_string();
let mut elems = Vec::new();
if cratename.len() <= 3 {
elems.push(&clen[..]);
}
match cratename.len() {
0 => panic!("0-length cratename"),
1 | 2 => {}
3 => elems.push(&cratename[0..1]),
_ => {
elems.push(&cratename[0..2]);
elems.push(&cratename[2..4]);
}
}
elems.push(cratename);
let ent = try_opt!(registry.get_name(elems[0]));
let obj = try_opt!(ent.to_object(registry_parent).ok());
let ent = try_opt!(try_opt!(obj.as_tree()).get_name(elems[1]));
let obj = try_opt!(ent.to_object(registry_parent).ok());
if elems.len() == 3 {
let ent = try_opt!(try_opt!(obj.as_tree()).get_name(elems[2]));
let obj = try_opt!(ent.to_object(registry_parent).ok());
Some(try_opt!(obj.as_blob()).content().into())
} else {
Some(try_opt!(obj.as_blob()).content().into())
}
}