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