From aafaa2fc2ee0dcd07e01d07e7c238428fb583799 Mon Sep 17 00:00:00 2001 From: Nick Cameron Date: Wed, 8 Nov 2017 07:59:14 +1300 Subject: [PATCH] Add git-fmt tool --- Cargo.toml | 3 + src/bin/git-fmt.rs | 193 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+) create mode 100644 src/bin/git-fmt.rs diff --git a/Cargo.toml b/Cargo.toml index 3af4803bc5f3b..975aff5e6cf09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,9 @@ name = "cargo-fmt" [[bin]] name = "rustfmt-format-diff" +[[bin]] +name = "git-fmt" + [features] default = ["cargo-fmt", "rustfmt-format-diff"] cargo-fmt = [] diff --git a/src/bin/git-fmt.rs b/src/bin/git-fmt.rs new file mode 100644 index 0000000000000..aa54c3b5718a8 --- /dev/null +++ b/src/bin/git-fmt.rs @@ -0,0 +1,193 @@ +extern crate env_logger; +extern crate getopts; +#[macro_use] +extern crate log; +extern crate rustfmt_nightly as rustfmt; + +use std::env; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::str::FromStr; + +use getopts::{Matches, Options}; + +use rustfmt::{run, Input}; +use rustfmt::config; + + +fn prune_files<'a>(files: Vec<&'a str>) -> Vec<&'a str> { + let prefixes: Vec<_> = files + .iter() + .filter(|f| f.ends_with("mod.rs") || f.ends_with("lib.rs")) + .map(|f| &f[..f.len() - 6]) + .collect(); + + let mut pruned_prefixes = vec![]; + for p1 in prefixes.into_iter() { + let mut include = true; + if !p1.starts_with("src/bin/") { + for p2 in pruned_prefixes.iter() { + if p1.starts_with(p2) { + include = false; + break; + } + } + } + if include { + pruned_prefixes.push(p1); + } + } + debug!("prefixes: {:?}", pruned_prefixes); + + files + .into_iter() + .filter(|f| { + let mut include = true; + if f.ends_with("mod.rs") || f.ends_with("lib.rs") || f.starts_with("src/bin/") { + return true; + } + for pp in pruned_prefixes.iter() { + if f.starts_with(pp) { + include = false; + break; + } + } + include + }) + .collect() +} + +fn git_diff(commits: String) -> String { + let mut cmd = Command::new("git"); + cmd.arg("diff"); + if commits != "0" { + cmd.arg(format!("HEAD~{}", commits)); + } + let output = cmd.output().expect("Couldn't execute `git diff`"); + String::from_utf8_lossy(&output.stdout).into_owned() +} + +fn get_files(input: &str) -> Vec<&str> { + input + .lines() + .filter(|line| line.starts_with("+++ b/") && line.ends_with(".rs")) + .map(|line| &line[6..]) + .collect() +} + +fn fmt_files(files: &[&str]) -> i32 { + let (config, _) = config::Config::from_resolved_toml_path(Path::new(".")) + .unwrap_or_else(|_| (config::Config::default(), None)); + + let mut exit_code = 0; + for file in files { + let summary = run(Input::File(PathBuf::from(file)), &config); + if !summary.has_no_errors() { + exit_code = 1; + } + } + exit_code +} + +fn uncommitted_files() -> Vec { + let mut cmd = Command::new("git"); + cmd.arg("ls-files"); + cmd.arg("--others"); + cmd.arg("--modified"); + cmd.arg("--exclude-standard"); + let output = cmd.output().expect("Couldn't execute Git"); + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.lines().map(|s| s.to_owned()).collect() +} + +fn check_uncommitted() { + let uncommitted = uncommitted_files(); + debug!("uncommitted files: {:?}", uncommitted); + if !uncommitted.is_empty() { + println!("Found untracked changes:"); + for f in uncommitted.iter() { + println!(" {}", f); + } + println!("Commit your work, or run with `-u`."); + println!("Exiting."); + std::process::exit(1); + } +} + +fn make_opts() -> Options { + let mut opts = Options::new(); + opts.optflag("h", "help", "show this message"); + opts.optflag("c", "check", "check only, don't format (unimplemented)"); + opts.optflag("u", "uncommitted", "format uncommitted files"); + opts +} + +struct Config { + commits: String, + uncommitted: bool, + check: bool, +} + +impl Config { + fn from_args(matches: &Matches, opts: &Options) -> Config { + // `--help` display help message and quit + if matches.opt_present("h") { + let message = format!( + "\nusage: {} [options]\n\n\ + commits: number of commits to format, default: 1", + env::args_os().next().unwrap().to_string_lossy() + ); + println!("{}", opts.usage(&message)); + std::process::exit(0); + } + + let mut config = Config { + commits: "1".to_owned(), + uncommitted: false, + check: false, + }; + + if matches.opt_present("c") { + config.check = true; + unimplemented!(); + } + + if matches.opt_present("u") { + config.uncommitted = true; + } + + if matches.free.len() > 1 { + panic!("unknown arguments, use `-h` for usage"); + } + if matches.free.len() == 1 { + let commits = matches.free[0].trim(); + if let Err(_) = u32::from_str(&commits) { + panic!("Couldn't parse number of commits"); + } + config.commits = commits.to_owned(); + } + + config + } +} + +fn main() { + let _ = env_logger::init(); + + let opts = make_opts(); + let matches = opts.parse(env::args().skip(1)) + .expect("Couldn't parse command line"); + let config = Config::from_args(&matches, &opts); + + if !config.uncommitted { + check_uncommitted(); + } + + let stdout = git_diff(config.commits); + let files = get_files(&stdout); + debug!("files: {:?}", files); + let files = prune_files(files); + debug!("pruned files: {:?}", files); + let exit_code = fmt_files(&files); + std::process::exit(exit_code); +}