]> git.lizzy.rs Git - rust.git/blob - src/git-rustfmt/main.rs
Merge pull request #2509 from topecongiro/issue-2493
[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::{config, run, 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         let mut include = true;
36         if !p1.starts_with("src/bin/") {
37             for p2 in &pruned_prefixes {
38                 if p1.starts_with(p2) {
39                     include = false;
40                     break;
41                 }
42             }
43         }
44         if include {
45             pruned_prefixes.push(p1);
46         }
47     }
48     debug!("prefixes: {:?}", pruned_prefixes);
49
50     files
51         .into_iter()
52         .filter(|f| {
53             let mut include = true;
54             if f.ends_with("mod.rs") || f.ends_with("lib.rs") || f.starts_with("src/bin/") {
55                 return true;
56             }
57             for pp in &pruned_prefixes {
58                 if f.starts_with(pp) {
59                     include = false;
60                     break;
61                 }
62             }
63             include
64         })
65         .collect()
66 }
67
68 fn git_diff(commits: &str) -> String {
69     let mut cmd = Command::new("git");
70     cmd.arg("diff");
71     if commits != "0" {
72         cmd.arg(format!("HEAD~{}", commits));
73     }
74     let output = cmd.output().expect("Couldn't execute `git diff`");
75     String::from_utf8_lossy(&output.stdout).into_owned()
76 }
77
78 fn get_files(input: &str) -> Vec<&str> {
79     input
80         .lines()
81         .filter(|line| line.starts_with("+++ b/") && line.ends_with(".rs"))
82         .map(|line| &line[6..])
83         .collect()
84 }
85
86 fn fmt_files(files: &[&str]) -> i32 {
87     let (config, _) = config::Config::from_resolved_toml_path(Path::new("."))
88         .unwrap_or_else(|_| (config::Config::default(), None));
89
90     let mut exit_code = 0;
91     for file in files {
92         let summary = run(Input::File(PathBuf::from(file)), &config);
93         if !summary.has_no_errors() {
94             exit_code = 1;
95         }
96     }
97     exit_code
98 }
99
100 fn uncommitted_files() -> Vec<String> {
101     let mut cmd = Command::new("git");
102     cmd.arg("ls-files");
103     cmd.arg("--others");
104     cmd.arg("--modified");
105     cmd.arg("--exclude-standard");
106     let output = cmd.output().expect("Couldn't execute Git");
107     let stdout = String::from_utf8_lossy(&output.stdout);
108     stdout
109         .lines()
110         .filter(|s| s.ends_with(".rs"))
111         .map(|s| s.to_owned())
112         .collect()
113 }
114
115 fn check_uncommitted() {
116     let uncommitted = uncommitted_files();
117     debug!("uncommitted files: {:?}", uncommitted);
118     if !uncommitted.is_empty() {
119         println!("Found untracked changes:");
120         for f in &uncommitted {
121             println!("  {}", f);
122         }
123         println!("Commit your work, or run with `-u`.");
124         println!("Exiting.");
125         std::process::exit(1);
126     }
127 }
128
129 fn make_opts() -> Options {
130     let mut opts = Options::new();
131     opts.optflag("h", "help", "show this message");
132     opts.optflag("c", "check", "check only, don't format (unimplemented)");
133     opts.optflag("u", "uncommitted", "format uncommitted files");
134     opts
135 }
136
137 struct Config {
138     commits: String,
139     uncommitted: bool,
140     check: bool,
141 }
142
143 impl Config {
144     fn from_args(matches: &Matches, opts: &Options) -> Config {
145         // `--help` display help message and quit
146         if matches.opt_present("h") {
147             let message = format!(
148                 "\nusage: {} <commits> [options]\n\n\
149                  commits: number of commits to format, default: 1",
150                 env::args_os().next().unwrap().to_string_lossy()
151             );
152             println!("{}", opts.usage(&message));
153             std::process::exit(0);
154         }
155
156         let mut config = Config {
157             commits: "1".to_owned(),
158             uncommitted: false,
159             check: false,
160         };
161
162         if matches.opt_present("c") {
163             config.check = true;
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     env_logger::init();
188
189     let opts = make_opts();
190     let matches = opts.parse(env::args().skip(1))
191         .expect("Couldn't parse command line");
192     let config = Config::from_args(&matches, &opts);
193
194     if !config.uncommitted {
195         check_uncommitted();
196     }
197
198     let stdout = git_diff(&config.commits);
199     let files = get_files(&stdout);
200     debug!("files: {:?}", files);
201     let files = prune_files(files);
202     debug!("pruned files: {:?}", files);
203     let exit_code = fmt_files(&files);
204     std::process::exit(exit_code);
205 }