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