├── .gitignore ├── Cargo.toml ├── README.md └── src └── bin.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "git-jira-tools" 3 | version = "0.2.0" 4 | authors = ["Eunchong Yu "] 5 | 6 | [[bin]] 7 | name = "git-jira" 8 | path = "src/bin.rs" 9 | 10 | [dependencies] 11 | docopt = "*" 12 | log = "*" 13 | hyper = ">=0.5.2" 14 | regex = "*" 15 | rustc-serialize = "*" 16 | url = "*" 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Usage 2 | ----- 3 | 4 | ```zsh 5 | $ # after cloning it 6 | $ cargo build --release 7 | $ # copy or link ./target/release/git-jira to $PATH 8 | $ # cd to your git repository 9 | $ git jira branch 10 | JIRA URL: 11 | Username: 12 | Password: 13 | bugfix/AD-224 Exception on /admin/users [POST] 14 | feature/CL-337 Add a new fascinate feature 15 | bugfix/MM-550 Response is too slow on /data/ 16 | * master 17 | improve/TR-736 Rewrite app/trashes.js 18 | ``` 19 | 20 | Configurations will be stored to `.gitconfig` in your home directory, so you don't need to type your username and password again. 21 | -------------------------------------------------------------------------------- /src/bin.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | extern crate docopt; 5 | extern crate hyper; 6 | extern crate regex; 7 | extern crate rustc_serialize; 8 | extern crate url; 9 | 10 | use std::collections::BTreeMap; 11 | use std::error::Error; 12 | use std::fmt; 13 | use std::io; 14 | use std::io::{BufRead, Write}; 15 | use std::process::Command; 16 | use std::str::FromStr; 17 | 18 | use docopt::Docopt; 19 | use hyper::Client; 20 | use hyper::header::{Authorization, Basic}; 21 | use regex::Regex; 22 | use rustc_serialize::json::Json; 23 | use url::{Url, UrlParser}; 24 | 25 | 26 | static USAGE: &'static str = r#" 27 | Usage: git-jira branch 28 | git-jira --help 29 | 30 | Options: 31 | -h, --help Show this message. 32 | "#; 33 | 34 | #[derive(RustcDecodable, Debug)] 35 | struct Args { 36 | cmd_branch: bool, 37 | } 38 | 39 | struct Config { 40 | base_url: Url, 41 | credential: hyper::header::Basic, 42 | } 43 | 44 | impl Config { 45 | fn from_git_config() -> Result> { 46 | let base_url = try!(read_config_value("com.spoqa.jira.url", "JIRA URL")); 47 | let base_url = Url::parse(&base_url).unwrap(); 48 | let cred = try!(read_credential()); 49 | Ok(Config { 50 | base_url: base_url, 51 | credential: cred, 52 | }) 53 | } 54 | } 55 | 56 | fn read_value(prompt: &str) -> Result> { 57 | let mut stdin = io::stdin(); 58 | let mut stdout = io::stdout(); 59 | print!("{}: ", prompt); 60 | try!(stdout.flush()); 61 | let mut line = String::new(); 62 | try!(stdin.read_line(&mut line)); 63 | Ok(line.trim().to_owned()) 64 | } 65 | 66 | fn load_config_value(key: &str) -> Result, Box> { 67 | let output = try!(Command::new("git").arg("config").arg(key).output()); 68 | if !output.status.success() { 69 | Ok(None) 70 | } else { 71 | let value = try!(std::str::from_utf8(&output.stdout)).trim(); 72 | if value.is_empty() { 73 | Ok(None) 74 | } else { 75 | Ok(Some(value.to_owned())) 76 | } 77 | } 78 | } 79 | 80 | fn save_config_value(key: &str, value: &str) -> Result<(), Box> { 81 | try!(Command::new("git").arg("config").arg("--global").arg(key).arg(value).status()); 82 | Ok(()) 83 | } 84 | 85 | fn read_config_value(key: &str, prompt: &str) -> Result> { 86 | match try!(load_config_value(key)) { 87 | Some(value) => Ok(value), 88 | None => { 89 | let value = try!(read_value(prompt)); 90 | try!(save_config_value(key, &value)); 91 | Ok(value) 92 | } 93 | } 94 | } 95 | 96 | fn read_credential() -> Result> { 97 | let key = "com.spoqa.jira.credential"; 98 | if let Some(value) = try!(load_config_value(key)) { 99 | if let Ok(value) = FromStr::from_str(&value) { 100 | return Ok(value) 101 | } 102 | } 103 | let username = try!(read_value("Username")); 104 | let password = try!(read_value("Password")); 105 | let cred = hyper::header::Basic { 106 | username: username, 107 | password: Some(password), 108 | }; 109 | struct SchemeExporter<'a, S: 'a + hyper::header::Scheme>(&'a S); 110 | impl<'a, S: 'a> fmt::Display for SchemeExporter<'a, S> where S: hyper::header::Scheme { 111 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.0.fmt_scheme(f) } 112 | } 113 | try!(save_config_value(key, &format!("{}", SchemeExporter(&cred)))); 114 | Ok(cred) 115 | } 116 | 117 | fn main() { 118 | let args: Args = Docopt::new(USAGE).and_then(|d| d.decode()).unwrap_or_else(|e| e.exit()); 119 | let config = Config::from_git_config().unwrap(); 120 | if args.cmd_branch { 121 | branch(config); 122 | } 123 | } 124 | 125 | fn branch(config: Config) { 126 | let key_pattern = Regex::new(r"[A-Z]+-\d+").unwrap(); 127 | 128 | let output = Command::new("git").arg("branch").arg("--list").arg("--no-column") 129 | .output() 130 | .unwrap_or_else(|e| { panic!("failed to execute process: {}", e) }); 131 | if !output.status.success() { 132 | io::stderr().write_all(&output.stderr[..]).unwrap(); 133 | return; 134 | } 135 | 136 | let branches: Vec<_> = io::BufReader::new(&output.stdout[..]).lines().map(Result::unwrap).collect(); 137 | let keys: Vec<_> = branches.iter().map(|b| key_pattern.captures(&b).and_then(|caps| caps.at(0))).collect(); 138 | let mut url = UrlParser::new().base_url(&config.base_url).parse("/rest/api/2/search").unwrap(); 139 | url.set_query_from_pairs(vec![ 140 | ("jql", &format!("key in ({})", keys.iter().filter_map(|&e| e).collect::>().connect(","))[..]), 141 | ("fields", "summary"), 142 | ].into_iter()); 143 | debug!("URL: {}", url); 144 | let mut client = Client::new(); 145 | let mut res = client.get(url) 146 | .header(Authorization(config.credential)) 147 | .send().unwrap(); 148 | let json = Json::from_reader(&mut res).unwrap(); 149 | let issues = json["issues"].as_array().unwrap(); 150 | let summary_map: BTreeMap<_, _> = issues.iter().map(|e| (e["key"].as_string().unwrap(), e["fields"]["summary"].as_string().unwrap())).collect(); 151 | debug!("response JSON:\n{}", json.pretty()); 152 | for (b, k) in branches.iter().zip(&keys) { 153 | let summary = match *k { 154 | Some(k) => summary_map[k], 155 | None => "", 156 | }; 157 | println!("{} \t{}", b, summary); 158 | } 159 | } 160 | --------------------------------------------------------------------------------