about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMatthias Krüger <matthias.krueger@famsik.de>2022-12-28 22:22:20 +0100
committerGitHub <noreply@github.com>2022-12-28 22:22:20 +0100
commit06306770f83404fe5f73658d3e041768ec5736af (patch)
tree88e5fbe0f51bbb8b3498c81cbc2f0740c6537d2d
parentc52d58f346aea2e2e7ed650ee95785d33500a6d0 (diff)
parent0b942a879d438216e90304436e74fa1ebb3ac16c (diff)
downloadrust-06306770f83404fe5f73658d3e041768ec5736af.tar.gz
rust-06306770f83404fe5f73658d3e041768ec5736af.zip
Rollup merge of #105702 - albertlarsan68:x-fmt-opt, r=jyn514
Format only modified files

As discussed on #105688, this makes x fmt only format modified files.

Fixes #105688
-rw-r--r--src/bootstrap/CHANGELOG.md1
-rw-r--r--src/bootstrap/clean.rs1
-rw-r--r--src/bootstrap/format.rs97
3 files changed, 98 insertions, 1 deletions
diff --git a/src/bootstrap/CHANGELOG.md b/src/bootstrap/CHANGELOG.md
index 64b74ecc9de..4105fa5ec96 100644
--- a/src/bootstrap/CHANGELOG.md
+++ b/src/bootstrap/CHANGELOG.md
@@ -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
 
diff --git a/src/bootstrap/clean.rs b/src/bootstrap/clean.rs
index 303c5603be7..8e363ee1290 100644
--- a/src/bootstrap/clean.rs
+++ b/src/bootstrap/clean.rs
@@ -94,6 +94,7 @@ fn clean_default(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() {
diff --git a/src/bootstrap/format.rs b/src/bootstrap/format.rs
index b2f6afead79..b49322e3c02 100644
--- a/src/bootstrap/format.rs
+++ b/src/bootstrap/format.rs
@@ -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};
@@ -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>,
@@ -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");
         }
@@ -187,4 +279,7 @@ pub fn format(build: &Builder<'_>, check: bool, paths: &[PathBuf]) {
     drop(tx);
 
     thread.join().unwrap();
+    if !check {
+        update_rustfmt_version(build);
+    }
 }