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