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