#![feature(rustc_private)]
use std::env;
use std::io::stdout;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
use getopts::{Matches, Options};
use rustfmt_nightly as rustfmt;
use tracing::debug;
use tracing_subscriber::EnvFilter;
use crate::rustfmt::{
CliOptions, FormatReportFormatterBuilder, Input, Session, Version, load_config,
};
fn prune_files(files: Vec<&str>) -> Vec<&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 {
if p1.starts_with("src/bin/") || pruned_prefixes.iter().all(|p2| !p1.starts_with(p2)) {
pruned_prefixes.push(p1);
}
}
debug!("prefixes: {:?}", pruned_prefixes);
files
.into_iter()
.filter(|f| {
if f.ends_with("mod.rs") || f.ends_with("lib.rs") || f.starts_with("src/bin/") {
return true;
}
pruned_prefixes.iter().all(|pp| !f.starts_with(pp))
})
.collect()
}
fn git_diff(commits: &str) -> 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, _) =
load_config::<NullOptions>(Some(Path::new(".")), None).expect("couldn't load config");
let mut exit_code = 0;
let mut out = stdout();
let mut session = Session::new(config, Some(&mut out));
for file in files {
let report = session.format(Input::File(PathBuf::from(file))).unwrap();
if report.has_warnings() {
eprintln!("{}", FormatReportFormatterBuilder::new(&report).build());
}
if !session.has_no_errors() {
exit_code = 1;
}
}
exit_code
}
struct NullOptions;
impl CliOptions for NullOptions {
fn apply_to(self, _: &mut rustfmt::Config) {
unreachable!();
}
fn config_path(&self) -> Option<&Path> {
unreachable!();
}
fn edition(&self) -> Option<rustfmt_nightly::Edition> {
unreachable!();
}
fn style_edition(&self) -> Option<rustfmt_nightly::StyleEdition> {
unreachable!();
}
fn version(&self) -> Option<Version> {
unreachable!();
}
}
fn uncommitted_files() -> Vec<String> {
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()
.filter(|s| s.ends_with(".rs"))
.map(std::borrow::ToOwned::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 {
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,
}
impl Config {
fn from_args(matches: &Matches, opts: &Options) -> Config {
if matches.opt_present("h") {
let message = format!(
"\nusage: {} <commits> [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,
};
if matches.opt_present("c") {
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 u32::from_str(commits).is_err() {
panic!("Couldn't parse number of commits");
}
config.commits = commits.to_owned();
}
config
}
}
fn main() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_env("RUSTFMT_LOG"))
.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);
}