Last active
August 29, 2015 14:13
-
-
Save adamhjk/6d10fcd0a90f3771f492 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![feature(phase)] | |
#![feature(old_orphan_check)] | |
extern crate regex; | |
#[macro_use] extern crate regex_macros; | |
extern crate "rustc-serialize" as rustc_serialize; | |
extern crate docopt; | |
#[macro_use] extern crate docopt_macros; | |
#[macro_use] extern crate log; | |
use std::os; | |
use std::error; | |
use utils::say::{say, sayln}; | |
use errors::{DeliveryError, Kind}; | |
pub mod errors; | |
pub mod git; | |
pub mod utils; | |
docopt!(Args derive Show, " | |
Usage: delivery review [--for=<pipeline>] | |
delivery checkout <change> [--for=<pipeline>] [--patchset=<number>] | |
delivery diff <change> [--for=<pipeline>] [--patchset=<number>] [--local] | |
delivery setup --user=<user> --server=<server> --ent=<ent> --org=<org> --project=<project> | |
delivery --help | |
Options: | |
-h, --help Show this message. | |
-f, --for=<pipeline> A pipeline to target [default: master] | |
-p, --patchset=<number> A patchset number [default: latest] | |
-u, --user=<user> A delivery username | |
-s, --server=<server> A delivery server | |
-e, --ent=<ent> A delivery enterprise | |
-o, --org=<org> A delivery organization | |
-p, --project=<project> The project name | |
-l, --local Diff against the local branch HEAD | |
<change> The change to checkout | |
"); | |
#[cfg(not(test))] | |
fn main() { | |
let args: Args = Args::docopt().decode().unwrap_or_else(|e| e.exit()); | |
debug!("{}", args); | |
let cmd_result = match args { | |
Args { | |
cmd_review: true, | |
flag_for: ref for_pipeline, | |
.. | |
} => review(for_pipeline.as_slice()), | |
Args { | |
cmd_setup: true, | |
flag_user: ref user, | |
flag_server: ref server, | |
flag_ent: ref ent, | |
flag_org: ref org, | |
flag_project: ref proj, | |
.. | |
} => setup(user.as_slice(), server.as_slice(), ent.as_slice(), org.as_slice(), proj.as_slice()), | |
Args { | |
cmd_checkout: true, | |
arg_change: ref change, | |
flag_patchset: ref patchset, | |
flag_for: ref pipeline, | |
.. | |
} => checkout(change.as_slice(), patchset.as_slice(), pipeline.as_slice()), | |
Args { | |
cmd_diff: true, | |
arg_change: ref change, | |
flag_patchset: ref patchset, | |
flag_for: ref pipeline, | |
flag_local: ref local, | |
.. | |
} => diff(change.as_slice(), patchset.as_slice(), pipeline.as_slice(), local), | |
_ => no_matching_command(), | |
}; | |
match cmd_result { | |
Ok(_) => {}, | |
Err(e) => exit_with(e, 1) | |
} | |
} | |
fn no_matching_command() -> Result<(), DeliveryError> { | |
Err(DeliveryError { kind: Kind::NoMatchingCommand, detail: None }) | |
} | |
fn exit_with<T: error::Error>(e: T, i: int) { | |
sayln("red", e.description()); | |
match e.detail() { | |
Some(deets) => sayln("red", deets.as_slice()), | |
None => {} | |
} | |
os::set_exit_status(i) | |
} | |
fn setup(user: &str, server: &str, ent: &str, org: &str, proj: &str) -> Result<(), DeliveryError> { | |
sayln("green", "Chef Delivery"); | |
try!(git::set_config(user, server, ent, org, proj)); | |
sayln("white", "Configuration added!"); | |
Ok(()) | |
} | |
fn review(for_pipeline: &str) -> Result<(), DeliveryError> { | |
sayln("green", "Chef Delivery"); | |
say("white", "Review for change "); | |
let head = try!(git::get_head()); | |
if for_pipeline == head.as_slice() { | |
return Err(DeliveryError{ kind: Kind::CannotReviewSameBranch, detail: None }) | |
} | |
say("yellow", head.as_slice()); | |
say("white", " targeted for pipeline "); | |
sayln("magenta", for_pipeline.as_slice()); | |
try!(git::git_push(head.as_slice(), for_pipeline)); | |
Ok(()) | |
} | |
fn checkout(change: &str, patchset: &str, pipeline: &str) -> Result<(), DeliveryError> { | |
sayln("green", "Chef Delivery"); | |
say("white", "Checking out "); | |
say("yellow", change.as_slice()); | |
say("white", " targeted for pipeline "); | |
say("magenta", pipeline.as_slice()); | |
if patchset == "latest" { | |
sayln("white", " tracking latest changes"); | |
} else { | |
say("white", " at patchset "); | |
sayln("yellow", patchset.as_slice()); | |
} | |
try!(git::checkout_review(change, patchset, pipeline)); | |
Ok(()) | |
} | |
fn diff(change: &str, patchset: &str, pipeline: &str, local: &bool) -> Result<(), DeliveryError> { | |
sayln("green", "Chef Delivery"); | |
say("white", "Showing diff for "); | |
say("yellow", change.as_slice()); | |
say("white", " targeted for pipeline "); | |
say("magenta", pipeline.as_slice()); | |
if patchset == "latest" { | |
sayln("white", " latest patchset"); | |
} else { | |
say("white", " at patchset "); | |
sayln("yellow", patchset.as_slice()); | |
} | |
try!(git::diff(change, patchset, pipeline, local)); | |
Ok(()) | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Compiling delivery v0.0.1 (file:///Users/adam/src/opscode/delivery/opscode/delivery-cli) | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/main.rs:2:12: 2:28 warning: feature is deprecated and will only be available for a limited time, please rewrite code that relies on it | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/main.rs:2 #![feature(old_orphan_check)] | |
^~~~~~~~~~~~~~~~ | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/git/mod.rs:23:17: 23:22 error: macro undefined: 'regex!' | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/git/mod.rs:23 let r = regex!(r"(.) (.+)"); | |
^~~~~ | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/git/mod.rs:126:21: 126:26 error: macro undefined: 'regex!' | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/git/mod.rs:126 let r = regex!(r"remote: (.+)"); | |
^~~~~ | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/git/mod.rs:141:17: 141:22 error: macro undefined: 'regex!' | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/git/mod.rs:141 let r = regex!(r"(.)\t(.+):(.+)\t\[(.+)\]"); | |
^~~~~ | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/main.rs:19:1: 19:7 error: macro undefined: 'docopt!' | |
/Users/adam/src/opscode/delivery/opscode/delivery-cli/src/main.rs:19 docopt!(Args derive Show, " | |
^~~~~~ | |
error: aborting due to 4 previous errors | |
Could not compile `delivery`. | |
To learn more, run the command again with --verbose. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extern crate regex; | |
extern crate "rustc-serialize" as rustc_serialize; | |
extern crate log; | |
pub use errors; | |
use std::io::process::Command; | |
use utils::say::{say, sayln, Spinner}; | |
use errors::{DeliveryError, Kind}; | |
pub fn get_head() -> Result<String, DeliveryError> { | |
let gitr = try!(git_command(&["branch"])); | |
let result = try!(parse_get_head(gitr.stdout.as_slice())); | |
Ok(result) | |
} | |
fn parse_get_head(stdout: &str) -> Result<String, DeliveryError> { | |
for line in stdout.lines_any() { | |
let r = regex!(r"(.) (.+)"); | |
let caps_result = r.captures(line); | |
let caps = match caps_result { | |
Some(caps) => caps, | |
None => { return Err(DeliveryError{ kind: Kind::BadGitOutputMatch, detail: Some(format!("Failed to match: {}", line)) }) } | |
}; | |
let token = caps.at(1).unwrap(); | |
if token == "*" { | |
let branch = caps.at(2).unwrap(); | |
return Ok(String::from_str(branch)); | |
} | |
} | |
return Err(DeliveryError{ kind: Kind::NotOnABranch, detail: None }); | |
} | |
#[test] | |
fn test_parse_get_head() { | |
let stdout = " adam/review | |
adam/test | |
adam/test6 | |
builder | |
first | |
foo | |
foo2 | |
* master | |
snazzy | |
testerton"; | |
let result = parse_get_head(stdout); | |
match result { | |
Ok(branch) => { | |
assert_eq!(branch.as_slice(), "master"); | |
}, | |
Err(e) => panic!("No result") | |
}; | |
} | |
pub struct GitResult { | |
stdout: String, | |
stderr: String | |
} | |
fn git_command(args: &[&str]) -> Result<GitResult, DeliveryError> { | |
let spinner = Spinner::start(); | |
let mut command = Command::new("git"); | |
command.args(args); | |
debug!("Git command: {}", command); | |
let output = match command.output() { | |
Ok(o) => o, | |
Err(e) => { spinner.stop(); return Err(DeliveryError{ kind: Kind::FailedToExecute, detail: Some(format!("failed to execute git: {}", e.desc))}) }, | |
}; | |
debug!("Git exited: {}", output.status); | |
spinner.stop(); | |
if !output.status.success() { | |
return Err(DeliveryError{ kind: Kind::GitFailed, detail: Some(format!("STDOUT: {}\nSTDERR: {}\n", String::from_utf8_lossy(output.output.as_slice()), String::from_utf8_lossy(output.error.as_slice())))}); | |
} | |
let stdout = String::from_utf8_lossy(output.output.as_slice()).to_string(); | |
debug!("Git stdout: {}", stdout); | |
let stderr = String::from_utf8_lossy(output.error.as_slice()).to_string(); | |
debug!("Git stderr: {}", stderr); | |
Ok(GitResult{ stdout: stdout, stderr: stderr }) | |
} | |
pub fn git_push(branch: &str, target: &str) -> Result<String, DeliveryError> { | |
let gitr = try!(git_command(&[ | |
"push", "--porcelain", "--progress", "--verbose", "delivery", format!("{}:_for/{}/{}", branch, target, branch).as_slice() | |
])); | |
let output = try!(parse_git_push_output(gitr.stdout.as_slice(), gitr.stderr.as_slice())); | |
for result in output.iter() { | |
match result.flag { | |
PushResultFlags::SuccessfulFastForward => sayln("green", format!("Updated change: {}", result.reason).as_slice()), | |
PushResultFlags::SuccessfulForcedUpdate => sayln("green", format!("Force updated change: {}", result.reason).as_slice()), | |
PushResultFlags::SuccessfulDeletedRef => sayln("red", format!("Deleted change: {}", result.reason).as_slice()), | |
PushResultFlags::SuccessfulPushedNewRef => sayln("green", format!("Created change: {}", result.reason).as_slice()), | |
PushResultFlags::Rejected => sayln("red", format!("Rejected change: {}", result.reason).as_slice()), | |
PushResultFlags::UpToDate => sayln("yellow", format!("Nothing added to the existing change").as_slice()), | |
} | |
} | |
Ok(gitr.stdout.to_string()) | |
} | |
pub enum PushResultFlags { | |
SuccessfulFastForward, | |
SuccessfulForcedUpdate, | |
SuccessfulDeletedRef, | |
SuccessfulPushedNewRef, | |
Rejected, | |
UpToDate, | |
} | |
impl Copy for PushResultFlags { } | |
pub struct PushResult { | |
flag: PushResultFlags, | |
from: String, | |
to: String, | |
reason: String | |
} | |
pub fn parse_git_push_output(push_output: &str, push_error: &str) -> Result<Vec<PushResult>, DeliveryError> { | |
let mut push_results: Vec<PushResult> = Vec::new(); | |
for line in push_error.lines_any() { | |
debug!("error: {}", line); | |
if line.starts_with("remote") { | |
let r = regex!(r"remote: (.+)"); | |
let caps_result = r.captures(line); | |
match caps_result { | |
Some(caps) => sayln("white", format!("{}", caps.at(1).unwrap()).as_slice()), | |
None => {} | |
} | |
} | |
} | |
for line in push_output.lines_any() { | |
debug!("output: {}", line); | |
if line.starts_with("To") { | |
continue; | |
} else if line.starts_with("Done") { | |
continue; | |
} | |
let r = regex!(r"(.)\t(.+):(.+)\t\[(.+)\]"); | |
let caps_result = r.captures(line); | |
let caps = match caps_result { | |
Some(caps) => caps, | |
None => { return Err(DeliveryError{ kind: Kind::BadGitOutputMatch, detail: Some(format!("Failed to match: {}", line)) }) } | |
}; | |
let result_flag = match caps.at(1).unwrap() { | |
" " => PushResultFlags::SuccessfulFastForward, | |
"+" => PushResultFlags::SuccessfulForcedUpdate, | |
"-" => PushResultFlags::SuccessfulDeletedRef, | |
"*" => PushResultFlags::SuccessfulPushedNewRef, | |
"!" => PushResultFlags::Rejected, | |
"=" => PushResultFlags::UpToDate, | |
_ => { return Err(DeliveryError{ kind: Kind::BadGitOutputMatch, detail: Some(format!("Unknown result flag")) }) } | |
}; | |
push_results.push( | |
PushResult{ | |
flag: result_flag, | |
from: String::from_str(caps.at(2).unwrap()), | |
to: String::from_str(caps.at(3).unwrap()), | |
reason: String::from_str(caps.at(4).unwrap()) | |
} | |
) | |
} | |
Ok(push_results) | |
} | |
// #[test] | |
// fn test_parse_git_push_output_success() { | |
// let stdout = "To ssh://[email protected]/Users/adam/src/opscode/delivery/opscode/delivery-cli2 | |
// = refs/heads/foo:refs/heads/_for/master/foo [up to date] | |
// Done"; | |
// let stderr = "Pushing to ssh://adam@[email protected]:8989/Chef/adam_universe/delivery-cli | |
// Total 0 (delta 0), reused 0 (delta 0) | |
// remote: Patchset already up to date, nothing to do | |
// remote: https://172.31.6.130/e/Chef/#/organizations/adam_universe/projects/delivery-cli/changes/146a9573-1bd0-4a27-a106-528347761811 | |
// updating local tracking ref 'refs/remotes/origin/_for/master/adam/test6'"; | |
// let result = parse_git_push_output(stdout, stderr); | |
// match result { | |
// Ok(pr_vec) => { | |
// assert_eq!(pr_vec[0].from.as_slice(), "refs/heads/foo"); | |
// assert_eq!(pr_vec[0].to.as_slice(), "refs/heads/_for/master/foo"); | |
// assert_eq!(pr_vec[0].reason.as_slice(), "up to date"); | |
// }, | |
// Err(_) => panic!("No result") | |
// }; | |
// } | |
pub fn set_config(user: &str, server: &str, ent: &str, org: &str, proj: &str) -> Result<(), DeliveryError> { | |
let result = git_command(&["remote", "add", "delivery", format!("ssh://{}@{}@{}:8989/{}/{}/{}", user, ent, server, ent, org, proj).as_slice()]); | |
match result { | |
Ok(_) => return Ok(()), | |
Err(e) => { | |
match e.detail { | |
Some(msg) => { | |
if msg.contains("remote delivery already exists") { | |
return Err(DeliveryError{ kind: Kind::GitSetupFailed, detail: None }); | |
} | |
}, | |
None => { | |
return Err(e) | |
} | |
} | |
}, | |
} | |
Ok(()) | |
} | |
pub fn checkout_branch_name(change: &str, patchset: &str) -> String { | |
if patchset == "latest" { | |
return String::from_str(change); | |
} else { | |
return format!("{}/{}", change, patchset); | |
} | |
} | |
pub fn diff(change: &str, patchset: &str, pipeline: &str, local: &bool) -> Result<(), DeliveryError> { | |
try!(git_command(&["fetch", "delivery"])); | |
let mut first_branch = format!("delivery/{}", pipeline); | |
if *local { | |
first_branch = String::from_str("HEAD"); | |
} | |
let diff = try!(git_command(&["diff", "--color=always", first_branch.as_slice(), format!("delivery/_reviews/{}/{}/{}", pipeline, change, patchset).as_slice()])); | |
say("white", "\n"); | |
sayln("white", diff.stdout.as_slice()); | |
Ok(()) | |
} | |
pub fn checkout_review(change: &str, patchset: &str, pipeline: &str) -> Result<(), DeliveryError> { | |
try!(git_command(&["fetch", "delivery"])); | |
let branchname = checkout_branch_name(change, patchset); | |
let result = git_command(&["branch", "--track", branchname.as_slice(), format!("delivery/_reviews/{}/{}/{}", pipeline, change, patchset).as_slice()]); | |
match result { | |
Ok(_) => { | |
try!(git_command(&["checkout", branchname.as_slice()])); | |
return Ok(()) | |
}, | |
Err(e) => { | |
match e.detail { | |
Some(msg) => { | |
if msg.contains("already exists.") { | |
try!(git_command(&["checkout", branchname.as_slice()])); | |
sayln("white", "Branch already exists, checking it out."); | |
let r = try!(git_command(&["status"])); | |
sayln("white", r.stdout.as_slice()); | |
return Ok(()) | |
} else { | |
return Err(DeliveryError{kind: Kind::GitFailed, detail: Some(msg)}); | |
} | |
}, | |
None => { | |
return Err(e) | |
} | |
} | |
}, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment