|
1 | 1 | //! Runs rustfmt on the repository.
|
2 | 2 |
|
3 | 3 | use crate::builder::Builder;
|
4 |
| -use crate::util::{output, t}; |
| 4 | +use crate::util::{output, program_out_of_date, t}; |
5 | 5 | use ignore::WalkBuilder;
|
6 | 6 | use std::collections::VecDeque;
|
7 | 7 | use std::path::{Path, PathBuf};
|
@@ -44,6 +44,90 @@ fn rustfmt(src: &Path, rustfmt: &Path, paths: &[PathBuf], check: bool) -> impl F
|
44 | 44 | }
|
45 | 45 | }
|
46 | 46 |
|
| 47 | +fn get_rustfmt_version(build: &Builder<'_>) -> Option<(String, PathBuf)> { |
| 48 | + let stamp_file = build.out.join("rustfmt.stamp"); |
| 49 | + |
| 50 | + let mut cmd = Command::new(match build.initial_rustfmt() { |
| 51 | + Some(p) => p, |
| 52 | + None => return None, |
| 53 | + }); |
| 54 | + cmd.arg("--version"); |
| 55 | + let output = match cmd.output() { |
| 56 | + Ok(status) => status, |
| 57 | + Err(_) => return None, |
| 58 | + }; |
| 59 | + if !output.status.success() { |
| 60 | + return None; |
| 61 | + } |
| 62 | + Some((String::from_utf8(output.stdout).unwrap(), stamp_file)) |
| 63 | +} |
| 64 | + |
| 65 | +/// Return whether the format cache can be reused. |
| 66 | +fn verify_rustfmt_version(build: &Builder<'_>) -> bool { |
| 67 | + let Some((version, stamp_file)) = get_rustfmt_version(build) else {return false;}; |
| 68 | + !program_out_of_date(&stamp_file, &version) |
| 69 | +} |
| 70 | + |
| 71 | +/// Updates the last rustfmt version used |
| 72 | +fn update_rustfmt_version(build: &Builder<'_>) { |
| 73 | + let Some((version, stamp_file)) = get_rustfmt_version(build) else {return;}; |
| 74 | + t!(std::fs::write(stamp_file, version)) |
| 75 | +} |
| 76 | + |
| 77 | +/// Returns the files modified between the `merge-base` of HEAD and |
| 78 | +/// rust-lang/master and what is now on the disk. |
| 79 | +/// |
| 80 | +/// Returns `None` if all files should be formatted. |
| 81 | +fn get_modified_files(build: &Builder<'_>) -> Option<Vec<String>> { |
| 82 | + let Ok(remote) = get_rust_lang_rust_remote() else {return None;}; |
| 83 | + if !verify_rustfmt_version(build) { |
| 84 | + return None; |
| 85 | + } |
| 86 | + Some( |
| 87 | + output( |
| 88 | + build |
| 89 | + .config |
| 90 | + .git() |
| 91 | + .arg("diff-index") |
| 92 | + .arg("--name-only") |
| 93 | + .arg("--merge-base") |
| 94 | + .arg(&format!("{remote}/master")), |
| 95 | + ) |
| 96 | + .lines() |
| 97 | + .map(|s| s.trim().to_owned()) |
| 98 | + .collect(), |
| 99 | + ) |
| 100 | +} |
| 101 | + |
| 102 | +/// Finds the remote for rust-lang/rust. |
| 103 | +/// For example for these remotes it will return `upstream`. |
| 104 | +/// ```text |
| 105 | +/// origin https://github.com./Nilstrieb/rust.git (fetch) |
| 106 | +/// origin https://github.com./Nilstrieb/rust.git (push) |
| 107 | +/// upstream https://github.com./rust-lang/rust (fetch) |
| 108 | +/// upstream https://github.com./rust-lang/rust (push) |
| 109 | +/// ``` |
| 110 | +fn get_rust_lang_rust_remote() -> Result<String, String> { |
| 111 | + let mut git = Command::new("git"); |
| 112 | + git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]); |
| 113 | + |
| 114 | + let output = git.output().map_err(|err| format!("{err:?}"))?; |
| 115 | + if !output.status.success() { |
| 116 | + return Err("failed to execute git config command".to_owned()); |
| 117 | + } |
| 118 | + |
| 119 | + let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?; |
| 120 | + |
| 121 | + let rust_lang_remote = stdout |
| 122 | + .lines() |
| 123 | + .find(|remote| remote.contains("rust-lang")) |
| 124 | + .ok_or_else(|| "rust-lang/rust remote not found".to_owned())?; |
| 125 | + |
| 126 | + let remote_name = |
| 127 | + rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?; |
| 128 | + Ok(remote_name.into()) |
| 129 | +} |
| 130 | + |
47 | 131 | #[derive(serde::Deserialize)]
|
48 | 132 | struct RustfmtConfig {
|
49 | 133 | ignore: Vec<String>,
|
@@ -110,6 +194,14 @@ pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
|
110 | 194 | // preventing the latter from being formatted.
|
111 | 195 | ignore_fmt.add(&format!("!/{}", untracked_path)).expect(&untracked_path);
|
112 | 196 | }
|
| 197 | + if !check && paths.is_empty() { |
| 198 | + if let Some(files) = get_modified_files(build) { |
| 199 | + for file in files { |
| 200 | + println!("formatting modified file {file}"); |
| 201 | + ignore_fmt.add(&format!("/{file}")).expect(&file); |
| 202 | + } |
| 203 | + } |
| 204 | + } |
113 | 205 | } else {
|
114 | 206 | println!("Not in git tree. Skipping git-aware format checks");
|
115 | 207 | }
|
@@ -187,4 +279,7 @@ pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
|
187 | 279 | drop(tx);
|
188 | 280 |
|
189 | 281 | thread.join().unwrap();
|
| 282 | + if !check { |
| 283 | + update_rustfmt_version(build); |
| 284 | + } |
190 | 285 | }
|
0 commit comments