#![experimental]
use command::{CommandExt,to_res,to_procout};
use std::collections::HashMap;
use std::io::process::Command;
use self::HeadType::{NamedBranch,Detached};
use self::RefType::{Heads,Tags};
#[experimental]
#[deriving(PartialEq)]
enum HeadType {
NamedBranch(String),
Detached,
}
#[experimental]
#[deriving(PartialEq)]
enum RefType {
Heads,
Tags,
}
#[experimental]
pub struct GitCommand {
wd: Option<Path>,
env: Vec<(String,String)>,
verbose: bool,
}
impl GitCommand {
#[experimental]
pub fn new() -> GitCommand {
GitCommand{ wd: None, env: Vec::new(), verbose: false }
}
#[experimental]
pub fn wd(&mut self, wd: Path) -> &mut GitCommand {
self.wd = Some(wd);
self
}
#[experimental]
pub fn env(&mut self, env: (&str,&str)) -> &mut GitCommand {
let (k,v) = env;
self.env.push((k.to_string(), v.to_string()));
self
}
#[experimental]
pub fn verbose(&mut self, verbose: bool) -> &mut GitCommand {
self.verbose = verbose;
self
}
pub fn git_cmd<T>(&self, subcommand: &str,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
let mut cmd = CommandExt::new("git");
match self.wd {
Some(ref dir) => { cmd.wd(dir); },
None => { ; },
}
for &(ref k, ref v) in self.env.iter() {
cmd.env(k.as_slice(), v.as_slice());
}
cmd.header(self.verbose);
cmd.arg(subcommand);
match args {
None => { ; },
Some(a) => { cmd.args(a.as_slice()); },
}
cmd.exec(execfn)
}
#[experimental]
pub fn add<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("add", args, execfn)
}
#[experimental]
pub fn branch<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("branch", args, execfn)
}
#[experimental]
pub fn checkout<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("checkout", args, execfn)
}
#[experimental]
pub fn clone<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("clone", args, execfn)
}
#[experimental]
pub fn commit<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("commit", args, execfn)
}
#[experimental]
pub fn config<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("config", args, execfn)
}
#[experimental]
pub fn fetch<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
let args = match args {
None => Some(vec!["--all",
"-p",
"--recurse-submodules=on-demand"]),
Some(a) => Some(a),
};
self.git_cmd("fetch", args, execfn)
}
#[experimental]
pub fn init<T>(&self,
args: Option<Vec<&str>>,execfn: |Command| -> T) -> T {
self.git_cmd("init", args, execfn)
}
#[experimental]
pub fn ls_remote<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("ls-remote", args, execfn)
}
#[experimental]
pub fn pull<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("pull", args, execfn)
}
#[experimental]
pub fn push<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("push", args, execfn)
}
#[experimental]
pub fn remote<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("remote", args, execfn)
}
#[experimental]
pub fn rev_parse<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
self.git_cmd("rev-parse", args, execfn)
}
#[experimental]
pub fn submodule<T>(&self,
args: Option<Vec<&str>>, execfn: |Command| -> T) -> T {
let args = match args {
None => Some(vec!("update")),
Some(a) => Some(a),
};
self.git_cmd("submodule", args, execfn)
}
fn head_type(&self) -> HeadType {
match self.rev_parse(Some(vec!["--abbrev-ref", "HEAD"]), to_procout()) {
Ok(o) => {
let co = String::from_utf8_lossy(o.output.as_slice());
let res = co.trim();
if res == "HEAD" {
Detached
} else {
NamedBranch(res.to_string())
}
},
Err(e) => panic!("failed to execute process: {}", e),
}
}
fn is_remote_x(&self, name: &str, remote: &str, rt: &RefType) -> bool {
let mut args = Vec::new();
if *rt == RefType::Heads {
args.push("-h");
} else {
args.push("-t");
}
args.push("--exit-code");
args.push(remote);
args.push(name);
self.ls_remote(Some(args), to_res()).is_ok()
}
fn remotes_by_type(&self, name: &str, rt: RefType) -> HashMap<String,bool> {
let mut res = HashMap::new();
let rem = match self.remote(None, to_procout()) {
Ok(o) => String::from_utf8_lossy(o.output.as_slice()).into_owned(),
Err(e) => panic!("failed to execute process: {}", e),
};
for remote in rem.lines() {
res.insert(remote.to_string(), self.is_remote_x(name, remote, &rt));
}
res
}
#[experimental]
pub fn update_branch(&self, branch: &str) -> Result<u8,u8> {
match self.fetch(None, to_res()) {
Ok(_) => {
if !(self.head_type() == NamedBranch(branch.to_string())) {
match self.checkout(Some(vec![branch]), to_res())
.and(self.submodule(None, to_res())) {
Ok(status) => {
if !(self.head_type() == Detached) {
self.pull(None, to_res())
} else {
Ok(status)
}
},
Err(_) => {
println!("Remote Heads: {}",
self.remotes_by_type(branch,
RefType::Heads));
println!("Remote Tags: {}",
self.remotes_by_type(branch,
RefType::Tags));
Err(2)
},
}
} else {
self.pull(None, to_res())
}
},
Err(_) => Err(1),
}
}
}
#[cfg(test)]
mod test {
use command::to_res;
use super::GitCommand;
use std::io;
use std::io::{File,IoResult};
use std::io::fs::PathExtensions;
use std::io::fs::{mkdir_recursive,rmdir_recursive};
fn touch(path: &Path) -> IoResult<()> {
if !path.exists() {
File::create(path).and_then(|_| Ok(()))
} else {
Ok(())
}
}
fn tmp_repo(name: &str) -> String {
let mut dir = String::from_str("/tmp/");
dir.push_str(name);
dir.push_str(".git");
dir
}
fn init_bare(gcmd: &GitCommand, name: &str) -> Result<u8,u8> {
let repo = tmp_repo(name);
gcmd.init(Some(vec!["--bare", repo.as_slice()]), to_res())
}
fn clone_into(gcmd: &GitCommand,
name: &str,
remote: Option<&str>) -> Result<u8,u8> {
let repo = match remote {
None => tmp_repo(name),
Some(r) => r.to_string(),
};
gcmd.clone(Some(vec![repo.as_slice(), name]), to_res())
}
#[test]
fn test_git_workflow() {
let tmp = Path::new("/tmp");
if tmp.join("pulltest").exists() {
assert_eq!(Ok(()), rmdir_recursive(&tmp.join("pulltest")));
}
if tmp.join("pulltest1").exists() {
assert_eq!(Ok(()), rmdir_recursive(&tmp.join("pulltest1")));
}
if tmp.join("pulltest.git").exists() {
assert_eq!(Ok(()), rmdir_recursive(&tmp.join("pulltest.git")));
}
let mut gcmd = GitCommand::new();
gcmd.verbose(true);
assert_eq!(Ok(0), init_bare(&gcmd, "pulltest"));
let pt = Path::new("/tmp/pulltest");
assert_eq!(Ok(()), mkdir_recursive(&pt, io::USER_RWX));
gcmd.wd(Path::new("/tmp/pulltest"));
assert_eq!(Ok(0), gcmd.init(None, to_res()));
assert_eq!(Ok(0), gcmd.remote(Some(vec!["add",
"origin",
"/tmp/pulltest.git"]),
to_res()));
assert_eq!(Ok(()), touch(&pt.join("README.md")));
assert_eq!(Ok(0), gcmd.add(Some(vec!["."]), to_res()));
assert_eq!(Ok(0), gcmd.config(Some(vec!["user.email",
"jason.g.ozias@gmail.com"]),
to_res()));
assert_eq!(Ok(0), gcmd.config(Some(vec!["user.name", "Jason Ozias"]),
to_res()));
assert_eq!(Ok(0), gcmd.config(Some(vec!["push.default", "simple"]),
to_res()));
assert_eq!(Ok(0), gcmd.commit(Some(vec!["-m", "initial commit"]),
to_res()));
assert_eq!(Ok(0), gcmd.push(Some(vec!["-u", "origin", "master"]),
to_res()));
assert_eq!(Ok(0), gcmd.branch(Some(vec!["feature"]), to_res()));
assert_eq!(Ok(0), gcmd.checkout(Some(vec!["feature"]), to_res()));
assert_eq!(Ok(0), gcmd.push(Some(vec!["-u", "origin", "feature"]),
to_res()));
assert_eq!(Ok(0), gcmd.checkout(Some(vec!["master"]), to_res()));
gcmd.wd(Path::new("/tmp/pulltest"));
assert_eq!(Ok(0), gcmd.pull(None, to_res()));
gcmd.wd(Path::new("/tmp"));
assert_eq!(Ok(0), clone_into(&gcmd, "pulltest1", Some("pulltest")));
gcmd.wd(Path::new("/tmp/pulltest1"));
assert_eq!(Ok(0), gcmd.fetch(None, to_res()));
assert_eq!(Ok(0), gcmd.remote(Some(vec!["update"]), to_res()));
assert_eq!(Ok(0), gcmd.update_branch("master"));
let tmp = Path::new("/tmp");
if tmp.join("pulltest").exists() {
assert_eq!(Ok(()), rmdir_recursive(&tmp.join("pulltest")));
}
if tmp.join("pulltest1").exists() {
assert_eq!(Ok(()), rmdir_recursive(&tmp.join("pulltest1")));
}
if tmp.join("pulltest.git").exists() {
assert_eq!(Ok(()), rmdir_recursive(&tmp.join("pulltest.git")));
}
}
}