Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Format only modified files #105702

Merged
merged 5 commits into from
Dec 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/bootstrap/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Several unsupported `./configure` options have been removed: `optimize`, `parallel-compiler`. These can still be enabled with `--set`, although it isn't recommended.
- `remote-test-server`'s `verbose` argument has been removed in favor of the `--verbose` flag
- `remote-test-server`'s `remote` argument has been removed in favor of the `--bind` flag. Use `--bind 0.0.0.0:12345` to replicate the behavior of the `remote` argument.
- `x.py fmt` now formats only files modified between the merge-base of HEAD and the last commit in the master branch of the rust-lang repository and the current working directory. To restore old behaviour, use `x.py fmt .`. The check mode is not affected by this change. [#105702](https://github.com/rust-lang/rust/pull/105702)

### Non-breaking changes

Expand Down
1 change: 1 addition & 0 deletions src/bootstrap/clean.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub fn clean(build: &Build, all: bool) {
rm_rf(&build.out.join("tmp"));
rm_rf(&build.out.join("dist"));
rm_rf(&build.out.join("bootstrap"));
rm_rf(&build.out.join("rustfmt.stamp"));

for host in &build.hosts {
let entries = match build.out.join(host.triple).read_dir() {
Expand Down
97 changes: 96 additions & 1 deletion src/bootstrap/format.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Runs rustfmt on the repository.

use crate::builder::Builder;
use crate::util::{output, t};
use crate::util::{output, program_out_of_date, t};
use ignore::WalkBuilder;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -44,6 +44,90 @@ fn rustfmt(src: &Path, rustfmt: &Path, paths: &[PathBuf], check: bool) -> impl F
}
}

fn get_rustfmt_version(build: &Builder<'_>) -> Option<(String, PathBuf)> {
let stamp_file = build.out.join("rustfmt.stamp");

let mut cmd = Command::new(match build.initial_rustfmt() {
Some(p) => p,
None => return None,
});
cmd.arg("--version");
let output = match cmd.output() {
Ok(status) => status,
Err(_) => return None,
};
if !output.status.success() {
return None;
}
Some((String::from_utf8(output.stdout).unwrap(), stamp_file))
}

/// Return whether the format cache can be reused.
fn verify_rustfmt_version(build: &Builder<'_>) -> bool {
let Some((version, stamp_file)) = get_rustfmt_version(build) else {return false;};
!program_out_of_date(&stamp_file, &version)
}

/// Updates the last rustfmt version used
fn update_rustfmt_version(build: &Builder<'_>) {
let Some((version, stamp_file)) = get_rustfmt_version(build) else {return;};
t!(std::fs::write(stamp_file, version))
}

/// Returns the files modified between the `merge-base` of HEAD and
/// rust-lang/master and what is now on the disk.
///
/// Returns `None` if all files should be formatted.
fn get_modified_files(build: &Builder<'_>) -> Option<Vec<String>> {
let Ok(remote) = get_rust_lang_rust_remote() else {return None;};
if !verify_rustfmt_version(build) {
return None;
}
Some(
output(
build
.config
.git()
.arg("diff-index")
.arg("--name-only")
.arg("--merge-base")
.arg(&format!("{remote}/master")),
)
.lines()
.map(|s| s.trim().to_owned())
.collect(),
)
}

/// Finds the remote for rust-lang/rust.
/// For example for these remotes it will return `upstream`.
/// ```text
/// origin https://github.com/Nilstrieb/rust.git (fetch)
/// origin https://github.com/Nilstrieb/rust.git (push)
/// upstream https://github.com/rust-lang/rust (fetch)
/// upstream https://github.com/rust-lang/rust (push)
/// ```
fn get_rust_lang_rust_remote() -> Result<String, String> {
let mut git = Command::new("git");
git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]);

let output = git.output().map_err(|err| format!("{err:?}"))?;
if !output.status.success() {
return Err("failed to execute git config command".to_owned());
}

let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?;

let rust_lang_remote = stdout
.lines()
.find(|remote| remote.contains("rust-lang"))
.ok_or_else(|| "rust-lang/rust remote not found".to_owned())?;

let remote_name =
rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?;
Ok(remote_name.into())
}

#[derive(serde::Deserialize)]
struct RustfmtConfig {
ignore: Vec<String>,
Expand Down Expand Up @@ -110,6 +194,14 @@ pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
// preventing the latter from being formatted.
ignore_fmt.add(&format!("!/{}", untracked_path)).expect(&untracked_path);
}
if !check && paths.is_empty() {
if let Some(files) = get_modified_files(build) {
for file in files {
println!("formatting modified file {file}");
ignore_fmt.add(&format!("/{file}")).expect(&file);
}
}
}
} else {
println!("Not in git tree. Skipping git-aware format checks");
}
Expand Down Expand Up @@ -187,4 +279,7 @@ pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
drop(tx);

thread.join().unwrap();
if !check {
update_rustfmt_version(build);
}
}