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 | |
5 | use std::env; |
6 | use std::io::stdout; |
7 | use std::path::{Path, PathBuf}; |
8 | use std::process::Command; |
9 | use std::str::FromStr; |
10 | |
11 | use getopts::{Matches, Options}; |
12 | use rustfmt_nightly as rustfmt; |
13 | use tracing::debug; |
14 | use tracing_subscriber::EnvFilter; |
15 | |
16 | use crate::rustfmt::{ |
17 | CliOptions, FormatReportFormatterBuilder, Input, Session, Version, load_config, |
18 | }; |
19 | |
20 | fn 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 | |
46 | fn git_diff(commits: &str) -> String { |
47 | let mut cmd: Command = Command::new(program:"git" ); |
48 | cmd.arg("diff" ); |
49 | if commits != "0" { |
50 | cmd.arg(format!("HEAD~ {commits}" )); |
51 | } |
52 | let output: Output = cmd.output().expect(msg:"Couldn't execute `git diff`" ); |
53 | String::from_utf8_lossy(&output.stdout).into_owned() |
54 | } |
55 | |
56 | fn get_files(input: &str) -> Vec<&str> { |
57 | inputimpl Iterator |
58 | .lines() |
59 | .filter(|line: &&str| line.starts_with("+++ b/" ) && line.ends_with(".rs" )) |
60 | .map(|line: &str| &line[6..]) |
61 | .collect() |
62 | } |
63 | |
64 | fn fmt_files(files: &[&str]) -> i32 { |
65 | let (config: Config, _) = |
66 | load_config::<NullOptions>(Some(Path::new("." )), None).expect(msg:"couldn't load config" ); |
67 | |
68 | let mut exit_code: i32 = 0; |
69 | let mut out: Stdout = stdout(); |
70 | let mut session: Session<'_, Stdout> = Session::new(config, out:Some(&mut out)); |
71 | for file: &&str in files { |
72 | let report: FormatReport = 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 | |
83 | struct NullOptions; |
84 | |
85 | impl 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 | |
103 | fn uncommitted_files() -> Vec<String> { |
104 | let mut cmd: Command = Command::new(program:"git" ); |
105 | cmd.arg("ls-files" ); |
106 | cmd.arg("--others" ); |
107 | cmd.arg("--modified" ); |
108 | cmd.arg("--exclude-standard" ); |
109 | let output: Output = cmd.output().expect(msg:"Couldn't execute Git" ); |
110 | let stdout: Cow<'_, str> = String::from_utf8_lossy(&output.stdout); |
111 | stdoutimpl Iterator |
112 | .lines() |
113 | .filter(|s: &&str| s.ends_with(".rs" )) |
114 | .map(std::borrow::ToOwned::to_owned) |
115 | .collect() |
116 | } |
117 | |
118 | fn check_uncommitted() { |
119 | let uncommitted: Vec = uncommitted_files(); |
120 | debug!("uncommitted files: {:?}" , uncommitted); |
121 | if !uncommitted.is_empty() { |
122 | println!("Found untracked changes:" ); |
123 | for f: &String in &uncommitted { |
124 | println!(" {f}" ); |
125 | } |
126 | println!("Commit your work, or run with `-u`." ); |
127 | println!("Exiting." ); |
128 | std::process::exit(code:1); |
129 | } |
130 | } |
131 | |
132 | fn make_opts() -> Options { |
133 | let mut opts: Options = Options::new(); |
134 | opts.optflag(short_name:"h" , long_name:"help" , desc:"show this message" ); |
135 | opts.optflag(short_name:"c" , long_name:"check" , desc:"check only, don't format (unimplemented)" ); |
136 | opts.optflag(short_name:"u" , long_name:"uncommitted" , desc:"format uncommitted files" ); |
137 | opts |
138 | } |
139 | |
140 | struct Config { |
141 | commits: String, |
142 | uncommitted: bool, |
143 | } |
144 | |
145 | impl 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 | |
186 | fn main() { |
187 | tracing_subscriberSubscriberBuilder::fmt() |
188 | .with_env_filter(EnvFilter::from_env("RUSTFMT_LOG" )) |
189 | .init(); |
190 | |
191 | let opts: Options = make_opts(); |
192 | let matches: Matches = opts |
193 | .parse(env::args().skip(1)) |
194 | .expect(msg:"Couldn't parse command line" ); |
195 | let config: Config = Config::from_args(&matches, &opts); |
196 | |
197 | if !config.uncommitted { |
198 | check_uncommitted(); |
199 | } |
200 | |
201 | let stdout: String = git_diff(&config.commits); |
202 | let files: Vec<&str> = get_files(&stdout); |
203 | debug!("files: {:?}" , files); |
204 | let files: Vec<&str> = prune_files(files); |
205 | debug!("pruned files: {:?}" , files); |
206 | let exit_code: i32 = fmt_files(&files); |
207 | std::process::exit(exit_code); |
208 | } |
209 | |