extern crate std;
use config;
use compiler::parser;
use compiler::parser::PropertyValue;
use fetcher;
use subprocess;
const METHOD_NAME: &'static str = "git";
pub enum Revision {
Commit(String),
Branch(String),
Tag(String),
}
impl std::fmt::Display for Revision {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
Revision::Commit(ref string) => write!(f, "{}", string),
Revision::Branch(ref string) => write!(f, "{}", string),
Revision::Tag(ref string) => write!(f, "{}", string),
}
}
}
struct GitVersion {
major: usize,
minor: usize,
}
impl std::fmt::Display for GitVersion {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}.{}.x", self.major, self.minor)
}
}
pub struct Method {
urls: Vec<String>,
clone_recursive: bool,
shallow: bool,
shallow_submodules: bool,
revision: Revision,
remote: String,
}
fn git_command_new() -> std::process::Command {
subprocess::new("git")
}
impl Method {
fn is_repository_clean(&self, component: &config::Component) -> Result<bool, fetcher::Error> {
let mut cmd = git_command_new();
cmd.args(&["-C", component.path_get().as_str(),
"status",
"-z", "--porcelain", "-uall"]);
let stdout = try!(subprocess::run_get_stdout(&mut cmd));
Ok(stdout.is_empty())
}
fn fetch_new_branch(&self, component: &config::Component) -> Result<(), fetcher::Error> {
for url in &self.urls {
let revision = self.revision.to_string();
let remote = self.remote.to_string();
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("clone");
cmd.arg("--quiet");
cmd.arg("--origin");
cmd.arg(remote);
cmd.arg("--branch");
cmd.arg(revision.as_str());
if self.clone_recursive {
cmd.arg("--recursive");
}
if self.shallow {
cmd.arg("--depth=1");
}
if self.shallow_submodules {
cmd.arg("--shallow-submodules");
}
cmd.arg(url);
cmd.arg(component.path_get());
match subprocess::run(&mut cmd) {
Ok(_) => { return Ok(()); },
Err(_) => { error!("Fetcher failed"); }
}
}
error!("Unable to retrieve the component `{}`", component.name_get());
Err(fetcher::Error::EverythingFailed)
}
fn fetch_new_revision(&self, component: &config::Component) -> Result<(), fetcher::Error> {
for url in &self.urls {
let revision = self.revision.to_string();
let remote = self.remote.to_string();
{
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("clone");
cmd.arg("--quiet");
cmd.arg("--origin");
cmd.arg(remote);
cmd.arg("--no-checkout");
cmd.arg(url);
cmd.arg(component.path_get());
if subprocess::run(&mut cmd).is_err() {
error!("Fetcher (clone) failed");
continue;
}
}
try!(self.git_checkout(component, &revision));
if self.clone_recursive {
try!(self.git_submodule_update(component));
}
return Ok(());
}
error!("Unable to retrieve the component `{}`", component.name_get());
Err(fetcher::Error::EverythingFailed)
}
fn git_ls_remote(&self, component: &config::Component, param: &str) -> Result<String, fetcher::Error> {
let remote = self.remote.to_string();
let mut cmd = git_command_new();
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("ls-remote");
cmd.arg("--quiet");
cmd.arg("--refs");
cmd.arg(param);
cmd.arg(remote);
let stdout = try!(subprocess::run_get_stdout(&mut cmd));
Ok(stdout)
}
fn update_remote(&self, component: &config::Component, url: &str) -> Result<(), fetcher::Error> {
let remote = self.remote.to_string();
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("remote");
cmd.arg("set-url");
cmd.arg(remote);
cmd.arg(url);
try!(subprocess::run(&mut cmd));
Ok(())
}
fn create_remote(&self, component: &config::Component, url: &str) -> Result<(), fetcher::Error> {
let remote = self.remote.to_string();
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("remote");
cmd.arg("add");
cmd.arg(remote);
cmd.arg(url);
try!(subprocess::run(&mut cmd));
Ok(())
}
fn fixup_remote(&self, component: &config::Component, url: &str) -> Result<(), fetcher::Error> {
let mut cmd = git_command_new();
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("remote");
for remote in try!(subprocess::run_get_stdout(&mut cmd)).split('\n') {
if remote == self.remote {
return self.update_remote(component, url);
}
}
self.create_remote(component, &self.urls[0])
}
fn is_shallow(&self, component: &config::Component) -> Result<bool, fetcher::Error> {
let mut cmd = git_command_new();
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("rev-parse");
cmd.arg("--git-dir");
let mut git_dir = try!(subprocess::run_get_stdout(&mut cmd));
git_dir.pop();
let mut path = std::path::PathBuf::new();
path.push(git_dir.as_str());
path.push("shallow");
let is_shallow = path.as_path().exists();
Ok(is_shallow)
}
fn get_head_hash(&self, component: &config::Component) -> Result<String, fetcher::Error> {
let mut cmd = git_command_new();
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("rev-parse");
cmd.arg("HEAD");
let mut hash = try!(subprocess::run_get_stdout(&mut cmd));
hash.pop();
Ok(hash)
}
fn needs_updating(&self, component: &config::Component) -> Result<bool, fetcher::Error> {
let hash = try!(self.get_head_hash(component));
match self.revision {
Revision::Commit(ref commit) => {
trace!("Update (commit) from: {}, to: {}", hash, commit);
Ok(&hash != commit)
},
Revision::Tag(ref tag) => {
let tag_hash = try!(self.get_hash_for_ref(component, "tags", tag));
trace!("Update (tag) from: {}, to: {}", hash, tag_hash);
Ok(tag_hash != hash)
},
Revision::Branch(ref branch) => {
let branch_hash = try!(self.get_hash_for_ref(component, "heads", branch));
trace!("Update (branch) from: {}, to: {}", hash, branch_hash);
Ok(branch_hash != hash)
},
}
}
fn get_hash_for_ref(&self, component: &config::Component, reftype: &str, gref: &str) -> Result<String, fetcher::Error> {
let arg = format!("--{}", reftype);
let git_ref = format!("refs/{}/{}", reftype, gref);
let stdout = try!(self.git_ls_remote(component, arg.as_str()));
let mut iterator = stdout.split_whitespace().rev();
while let Some(it_ref) = iterator.next() {
if git_ref == it_ref {
if let Some(hash) = iterator.next() {
return Ok(String::from(hash));
}
}
}
Err(fetcher::Error::InvalidReference)
}
fn unshallow(&self, component: &config::Component) -> Result<(), fetcher::Error> {
{
let remote = format!("remote.{}", self.remote).to_string();
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("config");
cmd.arg("--local");
cmd.arg("--unset");
cmd.arg(remote);
try!(subprocess::run(&mut cmd));
}
{
let remote = format!("remote.{}.fetch", self.remote).to_string();
let remote_val = format!("+refs/heads/*:refs/remotes/{}/*", self.remote).to_string();
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("config");
cmd.arg("--unset");
cmd.arg(remote);
cmd.arg(remote_val);
try!(subprocess::run(&mut cmd));
}
{
let remote = self.remote.to_string();
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("fetch");
cmd.arg("--unshallow");
cmd.arg(remote);
try!(subprocess::run(&mut cmd));
}
Ok(())
}
fn git_submodule_update(&self, component: &config::Component) -> Result<(), fetcher::Error> {
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("submodule");
cmd.arg("update");
cmd.arg("--quiet");
cmd.arg("--init");
cmd.arg("--recursive");
if self.shallow_submodules {
cmd.arg("--depth=1");
}
try!(subprocess::run(&mut cmd));
Ok(())
}
fn git_checkout(&self, component: &config::Component, revision: &str) -> Result<(), fetcher::Error> {
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("checkout");
cmd.arg("--quiet");
cmd.arg(revision);
try!(subprocess::run(&mut cmd));
Ok(())
}
fn update(&self, component: &config::Component) -> Result<(), fetcher::Error> {
{
let remote = self.remote.to_string();
let mut cmd = git_command_new();
cmd.stdout(std::process::Stdio::null());
cmd.arg("-C");
cmd.arg(component.path_get());
cmd.arg("fetch");
cmd.arg("--tags");
cmd.arg(remote);
try!(subprocess::run(&mut cmd));
}
{
let revision = self.revision.to_string();
try!(self.git_checkout(component, &revision));
}
if self.clone_recursive {
try!(self.git_submodule_update(component));
}
Ok(())
}
}
impl fetcher::Method for Method {
fn fetch_new(&self, component: &config::Component) -> Result<(), fetcher::Error> {
match self.revision {
Revision::Branch(_) => self.fetch_new_branch(component),
_ => self.fetch_new_revision(component),
}
}
fn fetch_update(&self, component: &config::Component, force: bool) -> Result<(), fetcher::Error> {
let is_clean = try!(self.is_repository_clean(component));
if is_clean || force {
for url in &self.urls {
self.fixup_remote(component, url)?;
if try!(self.needs_updating(component)) {
if try!(self.is_shallow(component)) {
try!(self.unshallow(component));
}
if self.update(component).is_err() {
error!("Failed to update component \"{}\" with url \"{}\"",
component.id_get(), url);
continue;
};
} else {
info!("Component \"{}\" is already up-to-date.", component.id_get());
}
return Ok(());
}
Err(fetcher::Error::EverythingFailed)
} else {
Err(fetcher::Error::Dirty)
}
}
fn is_fetchable(&self, component: &config::Component) -> bool {
let mut cmd = git_command_new();
cmd.args(&["-C", component.path_get().as_str(),
"rev-parse",
"--git-dir"]);
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::null());
if subprocess::run(&mut cmd).is_ok() {
if let Ok(output) = cmd.output() {
let string = std::str::from_utf8(&output.stdout).unwrap();
let git_dir_path = std::path::Path::new(string);
if ! git_dir_path.has_root() {
return true;
}
}
}
false
}
fn name_get(&self) -> &str {
METHOD_NAME
}
}
type RevisionResult = Result<Option<Revision>, parser::Error>;
fn rev_prop_get(component: &str, cfg: &config::Config, propname: &str) -> Result<Option<String>, parser::Error> {
if let Some(prop) = cfg.get_fetch_property(component, METHOD_NAME, propname) {
match prop {
PropertyValue::StringValue(val) => { return Ok(Some(val.clone())); },
_ => { return Err(parser::Error::InvalidPropertyType); }
}
}
Ok(None)
}
fn commit_prop_get(component: &str, cfg: &config::Config) -> RevisionResult {
if let Some(commit) = try!(rev_prop_get(component, cfg, "commit")) {
Ok(Some(Revision::Commit(commit)))
} else {
Ok(None)
}
}
fn branch_prop_get(component: &str, cfg: &config::Config) -> RevisionResult {
if let Some(commit) = try!(rev_prop_get(component, cfg, "branch")) {
Ok(Some(Revision::Branch(commit)))
} else {
Ok(None)
}
}
fn tag_prop_get(component: &str, cfg: &config::Config) -> RevisionResult {
if let Some(commit) = try!(rev_prop_get(component, cfg, "tag")) {
Ok(Some(Revision::Tag(commit)))
} else {
Ok(None)
}
}
fn parse_revision(component: &str, cfg: &config::Config) -> Result<Revision, parser::Error> {
let mut count = 0;
let mut revision: Option<Revision> = None;
if let Some(rev) = try!(commit_prop_get(component, cfg)) {
revision = Some(rev);
count += 1;
}
if let Some(rev) = try!(branch_prop_get(component, cfg)) {
revision = Some(rev);
count += 1;
}
if let Some(rev) = try!(tag_prop_get(component, cfg)) {
revision = Some(rev);
count += 1;
}
if count > 1 {
error!("Conflicting properties: \"branch\", \"tag\", \"commit\" are mutually exclusive");
Err(parser::Error::ConflictingProperties)
} else {
revision.ok_or(parser::Error::MissingRequiredProperty)
}
}
fn parse_clone_recursive(component: &str, cfg: &config::Config) -> Result<bool, parser::Error> {
if let Some(prop) = cfg.get_fetch_property(component, METHOD_NAME, "clone-recursive") {
match prop {
PropertyValue::BooleanValue(val) => { return Ok(val); },
_ => { return Err(parser::Error::InvalidPropertyType); }
}
}
Ok(true)
}
fn parse_shallow(component: &str, cfg: &config::Config) -> Result<bool, parser::Error> {
if let Some(prop) = cfg.get_fetch_property(component, METHOD_NAME, "shallow") {
match prop {
PropertyValue::BooleanValue(val) => { return Ok(val); },
_ => { return Err(parser::Error::InvalidPropertyType); }
}
}
Ok(false)
}
fn parse_shallow_submodules(component: &str, cfg: &config::Config) -> Result<bool, parser::Error> {
if let Some(prop) = cfg.get_fetch_property(component, METHOD_NAME, "shallow-submodules") {
match prop {
PropertyValue::BooleanValue(val) => { return Ok(val); },
_ => { return Err(parser::Error::InvalidPropertyType); }
}
}
Ok(false)
}
fn parse_remote(component: &str, cfg: &config::Config) -> Result<String, parser::Error> {
if let Some(prop) = cfg.get_fetch_property(component, METHOD_NAME, "remote") {
match prop {
PropertyValue::StringValue(val) => { return Ok(val.clone()); },
_ => { return Err(parser::Error::InvalidPropertyType); }
}
}
Ok(String::from("subcomponent"))
}
fn git_version_extract(string: &str) -> Option<GitVersion> {
if let Some(index) = string.rfind(' ') {
let version = &string[index+1..string.len() - 1];
let parts: Vec<&str> = version.split('.').collect();
if parts.len() >= 2 {
if let Ok(major) = parts[0].parse() {
if let Ok(minor) = parts[1].parse() {
return Some(GitVersion {
major: major,
minor: minor,
})
}
}
}
}
None
}
fn git_fallback_version_new() -> GitVersion {
GitVersion {
major: 0,
minor: 0,
}
}
fn git_version_get() -> GitVersion {
let mut cmd = git_command_new();
cmd.arg("--version");
if let Ok(stdout) = subprocess::run_get_stdout(&mut cmd) {
if let Some(version) = git_version_extract(&stdout) {
return version;
}
}
warn!("Failed to determine the version of git. Using a safe fallback.");
git_fallback_version_new()
}
pub fn parse(component: &str, cfg: &config::Config) -> Result<Box<fetcher::Method>, parser::Error> {
let clone_recursive = try!(parse_clone_recursive(component, cfg));
let revision = try!(parse_revision(component, cfg));
let shallow = try!(parse_shallow(component, cfg));
let mut shallow_submodules = try!(parse_shallow_submodules(component, cfg));
let remote = try!(parse_remote(component, cfg));
let urls = try!(fetcher::parse_url(component, METHOD_NAME, cfg));
if let Revision::Branch(_) = revision {
} else {
if shallow {
error!("Only a branch can be cloned as shallow");
return Err(parser::Error::ConflictingProperties)
}
}
let git_version = git_version_get();
if git_version.major <= 2 && git_version.minor < 9 {
if shallow_submodules {
warn!("Shallow submodules are enabled for component {}, but the \
git version you are using ({}) has no support for them.",
component, git_version);
shallow_submodules = false;
}
}
Ok(Box::new(Method {
urls: urls,
clone_recursive: clone_recursive,
shallow: shallow,
shallow_submodules: shallow_submodules,
revision: revision,
remote: remote,
}))
}
#[cfg(test)]
mod tests {
extern crate std;
use fetcher;
fn check_version(string: &str, expected_major: usize, expected_minor: usize) -> bool {
let text = String::from(string);
match fetcher::git::git_version_extract(&text) {
Some(version) => {
assert!(version.major == expected_major &&
version.minor == expected_minor);
true
},
None => false,
}
}
fn check_error_version(string: &str) -> bool {
check_version(string, 0, 0)
}
#[test]
fn extract_correct_git_versions() {
assert!(check_version("git version 2.10.2", 2, 10));
assert!(check_version("git version 1.18.4", 1, 18));
assert!(check_version("git version 1.0.", 1, 0));
assert!(check_version("git version 1.8.5.6", 1, 8));
assert!(check_version("git version 2.10.0-rc3", 2, 10));
}
#[test]
fn extract_invalid_git_versions() {
assert!(! check_error_version("git version 2 10 2"));
assert!(! check_error_version("git version 1 18.4"));
assert!(! check_error_version("1.18.4"));
assert!(! check_error_version("scrambled"));
assert!(! check_error_version("git version 1.18-rc0"));
}
}