#[macro_use]
extern crate lazy_static;
extern crate regex;
use regex::Regex;
extern crate docopt;
extern crate time;
mod markdown;
static USAGE: &'static str = "
Collection of utilities for managing the rfc process.
Usage:
rfc new
rfc init
rfc list (active | pending)
rfc approve <rfc-id> <pr-id>
rfc implement <rfc-id> <pr-id>
rfc (-h | --help)
Options:
-h, --help Show this message
";
use docopt::Docopt;
use std::fs::{self, File};
use std::io::{self, BufReader, BufRead, Read, Write};
use std::env;
use std::ascii::AsciiExt;
static RFC_DIR: &'static str = "./rfcs";
lazy_static! {
static ref PR_PATH: String = pull_request_path();
}
lazy_static! {
static ref PROJECT_NAME: String = project_name();
}
static DEFAULT_TEMPLATE: &'static str = include_str!("../templates/default.md");
use std::process::Command;
macro_rules! cmd {
( $prog:expr, $($arg:expr),* ) => {{
run_command(Command::new($prog)
.args(&[
$( $arg, )*
]))
}}
}
macro_rules! map_err_display {
($thing:expr) => { $thing.map_err(|err| format!("{:}", err)) }
}
macro_rules! expect_file_line {
($thing:expr) => { $thing.expect(&format!("{}:{}", file!(), line!())) }
}
fn pull_request_path() -> String {
env::var("PR_PATH").ok()
.or_else(|| {
cmd!("git", "config", "--get", "remote.origin.url").ok()
.and_then(|url| {
let rx = Regex::new(r#"\Agit@github\.com:(.+)\.git(?:\r?\n)?\z"#).unwrap();
rx.captures(&url).map(|captures| {
String::from("https://github.com/")
+ captures.at(1).unwrap()
+ "/pull"
})
})
})
.unwrap_or_default()
}
fn project_name() -> String {
env::var("PROJECT_NAME").ok()
.unwrap_or_else(|| {
env::current_dir().unwrap()
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap()
})
}
fn main() {
let args = Docopt::new(USAGE)
.and_then(|dopt| dopt.parse())
.unwrap_or_else(|e| e.exit());
if args.get_bool("new") {
new_rfc();
return;
}
if args.get_bool("init") {
init_rfcs();
return;
}
if args.get_bool("list") {
if args.get_bool("active") {
let active = list_active_rfcs();
if active.len() == 0 { println!("No active rfcs.")}
else { println!("{}", list_active_rfcs().join("\n")) }
}
if args.get_bool("pending") {
let pending = list_pending_rfcs();
if pending.len() == 0 { println!("No pending rfcs.") }
else { println!("{}", pending.join("\n")) }
}
return;
}
if args.get_bool("approve") {
expect_file_line!(approve(args.get_str("<rfc-id>"), args.get_str("<pr-id>")));
return;
}
if args.get_bool("implement") {
expect_file_line!(implement(args.get_str("<rfc-id>"), args.get_str("<pr-id>")));
return;
}
}
fn approve(id: &str, pr_id: &str) -> Result<(), String> {
{ try!(map_err_display!(File::open(rfc_path(&id)))); };
if !try!(cmd!("git", "status", "-z")).is_empty() {
return Err("\
Cannot merge RFC until working tree is clean.\
\nCommit or stash all changes\
\n".to_string());
}
let branch = try!(cmd!("git", "rev-parse", "--abbrev-ref", "HEAD"))
.trim().to_string();
try!(cmd!("git", "fetch", "origin"));
try!(cmd!("git", "checkout", "master"));
try!(cmd!("git", "pull", "--rebase=preserve"));
try!(cmd!("git", "merge", "--no-commit", &branch));
try!(populate_rfc_pr(&rfc_path(&id), "RFC PR", &pr_id));
try!(cmd!("git", "add", &rfc_path(&id)));
let new_rfc_id = incr_rfc_id(id);
try!(cmd!("git", "mv", &rfc_path(id), &rfc_path(&new_rfc_id)));
try!(update_readme());
try!(cmd!("git", "add", README_PATH));
try!(cmd!("git", "commit", "--message", &format!("Approve RFC {}", id)));
Ok(())
}
fn implement(id: &str, pr_id: &str) -> Result<(), String> {
{ try!(map_err_display!(File::open(rfc_path(&id)))); };
if !try!(cmd!("git", "status", "-z")).is_empty() {
return Err("\
Cannot mark RFC as implemented until working tree is clean.\
\nCommit or stash all changes\
\n".to_string());
}
try!(populate_rfc_pr(&rfc_path(&id), "Implementation PR", &pr_id));
try!(cmd!("git", "add", &rfc_path(&id)));
try!(update_readme());
try!(cmd!("git", "add", README_PATH));
try!(cmd!("git", "commit", "--message", &format!("Implement RFC {}", id)));
Ok(())
}
fn populate_rfc_pr(filename: &str, tag: &str, pr_id: &str) -> Result<(), String> {
let mut buffer = {
let mut file = try!(map_err_display!(File::open(filename)));
let mut buffer = String::with_capacity(try!(map_err_display!(file.metadata())).len() as usize);
try!(map_err_display!(file.read_to_string(&mut buffer)));
buffer
};
let project_name: &String = &PROJECT_NAME;
let pr_path: &String = &PR_PATH;
buffer = buffer.replace(&format!("{}: (leave this empty)", tag),
&format!("{tag}: [{project_name}#{pr_id}]({pr_path}/{pr_id})",
tag = tag,
project_name = project_name,
pr_path = pr_path,
pr_id = pr_id));
let mut out_file = File::create(filename.clone()).unwrap();
map_err_display!(out_file.write_all(&buffer.into_bytes()))
}
fn run_command(command: &mut Command) -> Result<String, String> {
let output = try!(map_err_display!(command.output()));
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout[..]).into_owned())
} else {
Err(String::from_utf8_lossy(&output.stderr[..]).into_owned())
}
}
fn incr_rfc_id(id: &str) -> String {
let parts: Vec<&str> = id.splitn(2, '-').collect();
let rest = parts[1];
format!("{:04}-{}", max_accepted_rfc_num() + 1, rest)
}
fn rfc_path(id: &str) -> String {
format!("{}/{}.md", RFC_DIR, id)
}
fn new_rfc() {
let mut read_buffer = String::new();
match io::stdin().read_line(&mut read_buffer) {
Ok(0) => return,
Ok(_) => {},
Err(_) => return,
}
let re = regex::Regex::new(r###"([\s_]+)"###).unwrap();
let name0 = read_buffer.trim().to_ascii_lowercase();
let name = re.replace_all(&name0[..], "-");
cmd!("git", "checkout", "master").unwrap();
cmd!("git", "checkout", "-b", &rfc_branch_name(&name)).unwrap();
let timestamp = time::strftime("%Y-%m-%d", &time::now()).unwrap();
let mut template_file = File::open(RFC_DIR.to_owned() + "/0000-template.md").unwrap();
let mut template = String::new();
template_file.read_to_string(&mut template).unwrap();
template = template.replace("(fill me in with a unique ident, my_awesome_feature)", &name[..]);
template = template.replace("(fill me in with today's date, YYYY-MM-DD)", ×tamp[..]);
let out_filename = format!("{}/0000-{}.md", RFC_DIR, name);
let mut out_file = File::create(out_filename.clone()).unwrap();
out_file.write_all(&template.into_bytes()).unwrap();
cmd!("git", "add", "--intent-to-add", &out_filename).unwrap();
println!("{}", out_filename);
}
fn init_rfcs() {
setup_rfcs_dir();
update_readme()
.or_else(|_| fs::remove_dir_all("rfcs")).unwrap();
}
fn setup_rfcs_dir() {
fs::create_dir("rfcs").unwrap();
let mut file = fs::File::create("rfcs/0000-template.md").unwrap();
let mut buffer: Vec<u8> = DEFAULT_TEMPLATE.bytes().collect();
file.write_all(&mut buffer).unwrap();
}
fn rfc_branch_name(name: &str) -> String {
format!("rfc-{}", name)
}
fn list_pending_rfcs() -> Vec<String> {
let paths = fs::read_dir(RFC_DIR).unwrap();
let mut acc = vec!();
for path in paths {
let p = format!("{}",
path.unwrap().path().file_name().unwrap()
.to_string_lossy()
.rsplitn(2, '.').nth(1).unwrap());
if !p.contains("0000-") { continue }
if p.contains("0000-template") { continue }
acc.push(p)
}
acc
}
fn list_accepted_rfcs() -> Vec<String> {
fs::read_dir(RFC_DIR).unwrap()
.map(|path| format!("{}",
path.unwrap()
.path().file_name().unwrap()
.to_string_lossy()
.rsplitn(2, '.').nth(1).unwrap())
)
.filter(|path| !path.starts_with("0000-"))
.collect()
}
fn list_accepted_rfc_nums() -> Vec<String> {
list_accepted_rfcs().into_iter()
.map(|path| String::from(path.splitn(2, '-').next().unwrap()))
.collect()
}
fn is_implemented(filename: &str) -> bool {
let file = File::open(filename)
.expect(&format!("Unable to open {}", filename));
for line in BufReader::new(file).lines() {
let line = expect_file_line!(line);
if line.starts_with("- Implementation PR: ") {
return !line.ends_with("(leave this empty)");
}
}
panic!("No `- Implementation PR: ...` line found in {}", filename)
}
fn list_active_rfcs() -> Vec<String> {
list_accepted_rfcs().into_iter()
.filter(|ref string| !is_implemented(&rfc_path(&string)))
.collect()
}
fn markdown_link_list(rfcs: &Vec<(String, String)>) -> Vec<String> {
rfcs.iter()
.map(|&(ref rfc, ref path)| format!("- [{}]({})\n", rfc, path))
.collect()
}
fn markdown_active_rfcs() -> String {
markdown_link_list(
&list_active_rfcs().into_iter()
.map(|ref rfc| (rfc.clone(), rfc_path(rfc)))
.collect::<Vec<(String, String)>>()
).into_iter().collect()
}
static README_PATH: &'static str = "README.md";
fn update_readme() -> Result<(), String> {
let mut buffer = {
let mut file = try!(map_err_display!(File::open(README_PATH)));
let mut buffer = String::new();
try!(map_err_display!(file.read_to_string(&mut buffer)));
buffer
};
buffer = markdown::replace_or_append_section(
&buffer, "Active RFCs",
&format!("<!--- auto-generated section -->\n{}", markdown_active_rfcs())
);
let mut out_file = expect_file_line!(File::create(README_PATH));
map_err_display!(out_file.write_all(&buffer.into_bytes()))
}
fn max_accepted_rfc_num() -> usize {
list_accepted_rfc_nums().into_iter().map(|string_num| {
string_num.parse::<usize>().unwrap()
}).max().unwrap()
}