git_rustfmt/
main.rs

1// We need this feature as it changes `dylib` linking behavior and allows us to link to
2// `rustc_driver`.
3#![feature(rustc_private)]
4
5use std::env;
6use std::io::stdout;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9use std::str::FromStr;
10
11use getopts::{Matches, Options};
12use rustfmt_nightly as rustfmt;
13use tracing::debug;
14use tracing_subscriber::EnvFilter;
15
16use crate::rustfmt::{
17    CliOptions, FormatReportFormatterBuilder, Input, Session, Version, load_config,
18};
19
20fn prune_files(files: Vec<&str>) -> Vec<&str> {
21    let prefixes: Vec<_> = files
22        .iter()
23        .filter(|f| f.ends_with("mod.rs") || f.ends_with("lib.rs"))
24        .map(|f| &f[..f.len() - 6])
25        .collect();
26
27    let mut pruned_prefixes = vec![];
28    for p1 in prefixes {
29        if p1.starts_with("src/bin/") || pruned_prefixes.iter().all(|p2| !p1.starts_with(p2)) {
30            pruned_prefixes.push(p1);
31        }
32    }
33    debug!("prefixes: {:?}", pruned_prefixes);
34
35    files
36        .into_iter()
37        .filter(|f| {
38            if f.ends_with("mod.rs") || f.ends_with("lib.rs") || f.starts_with("src/bin/") {
39                return true;
40            }
41            pruned_prefixes.iter().all(|pp| !f.starts_with(pp))
42        })
43        .collect()
44}
45
46fn git_diff(commits: &str) -> String {
47    let mut cmd = Command::new("git");
48    cmd.arg("diff");
49    if commits != "0" {
50        cmd.arg(format!("HEAD~{commits}"));
51    }
52    let output = cmd.output().expect("Couldn't execute `git diff`");
53    String::from_utf8_lossy(&output.stdout).into_owned()
54}
55
56fn get_files(input: &str) -> Vec<&str> {
57    input
58        .lines()
59        .filter(|line| line.starts_with("+++ b/") && line.ends_with(".rs"))
60        .map(|line| &line[6..])
61        .collect()
62}
63
64fn fmt_files(files: &[&str]) -> i32 {
65    let (config, _) =
66        load_config::<NullOptions>(Some(Path::new(".")), None).expect("couldn't load config");
67
68    let mut exit_code = 0;
69    let mut out = stdout();
70    let mut session = Session::new(config, Some(&mut out));
71    for file in files {
72        let report = session.format(Input::File(PathBuf::from(file))).unwrap();
73        if report.has_warnings() {
74            eprintln!("{}", FormatReportFormatterBuilder::new(&report).build());
75        }
76        if !session.has_no_errors() {
77            exit_code = 1;
78        }
79    }
80    exit_code
81}
82
83struct NullOptions;
84
85impl CliOptions for NullOptions {
86    fn apply_to(self, _: &mut rustfmt::Config) {
87        unreachable!();
88    }
89    fn config_path(&self) -> Option<&Path> {
90        unreachable!();
91    }
92    fn edition(&self) -> Option<rustfmt_nightly::Edition> {
93        unreachable!();
94    }
95    fn style_edition(&self) -> Option<rustfmt_nightly::StyleEdition> {
96        unreachable!();
97    }
98    fn version(&self) -> Option<Version> {
99        unreachable!();
100    }
101}
102
103fn uncommitted_files() -> Vec<String> {
104    let mut cmd = Command::new("git");
105    cmd.arg("ls-files");
106    cmd.arg("--others");
107    cmd.arg("--modified");
108    cmd.arg("--exclude-standard");
109    let output = cmd.output().expect("Couldn't execute Git");
110    let stdout = String::from_utf8_lossy(&output.stdout);
111    stdout
112        .lines()
113        .filter(|s| s.ends_with(".rs"))
114        .map(std::borrow::ToOwned::to_owned)
115        .collect()
116}
117
118fn check_uncommitted() {
119    let uncommitted = uncommitted_files();
120    debug!("uncommitted files: {:?}", uncommitted);
121    if !uncommitted.is_empty() {
122        println!("Found untracked changes:");
123        for f in &uncommitted {
124            println!("  {f}");
125        }
126        println!("Commit your work, or run with `-u`.");
127        println!("Exiting.");
128        std::process::exit(1);
129    }
130}
131
132fn make_opts() -> Options {
133    let mut opts = Options::new();
134    opts.optflag("h", "help", "show this message");
135    opts.optflag("c", "check", "check only, don't format (unimplemented)");
136    opts.optflag("u", "uncommitted", "format uncommitted files");
137    opts
138}
139
140struct Config {
141    commits: String,
142    uncommitted: bool,
143}
144
145impl Config {
146    fn from_args(matches: &Matches, opts: &Options) -> Config {
147        // `--help` display help message and quit
148        if matches.opt_present("h") {
149            let message = format!(
150                "\nusage: {} <commits> [options]\n\n\
151                 commits: number of commits to format, default: 1",
152                env::args_os().next().unwrap().to_string_lossy()
153            );
154            println!("{}", opts.usage(&message));
155            std::process::exit(0);
156        }
157
158        let mut config = Config {
159            commits: "1".to_owned(),
160            uncommitted: false,
161        };
162
163        if matches.opt_present("c") {
164            unimplemented!();
165        }
166
167        if matches.opt_present("u") {
168            config.uncommitted = true;
169        }
170
171        if matches.free.len() > 1 {
172            panic!("unknown arguments, use `-h` for usage");
173        }
174        if matches.free.len() == 1 {
175            let commits = matches.free[0].trim();
176            if u32::from_str(commits).is_err() {
177                panic!("Couldn't parse number of commits");
178            }
179            config.commits = commits.to_owned();
180        }
181
182        config
183    }
184}
185
186fn main() {
187    tracing_subscriber::fmt()
188        .with_env_filter(EnvFilter::from_env("RUSTFMT_LOG"))
189        .init();
190
191    let opts = make_opts();
192    let matches = opts
193        .parse(env::args().skip(1))
194        .expect("Couldn't parse command line");
195    let config = Config::from_args(&matches, &opts);
196
197    if !config.uncommitted {
198        check_uncommitted();
199    }
200
201    let stdout = git_diff(&config.commits);
202    let files = get_files(&stdout);
203    debug!("files: {:?}", files);
204    let files = prune_files(files);
205    debug!("pruned files: {:?}", files);
206    let exit_code = fmt_files(&files);
207    std::process::exit(exit_code);
208}