about summary refs log tree commit diff
diff options
context:
space:
mode:
authorNilstrieb <48135649+Nilstrieb@users.noreply.github.com>2022-12-14 20:22:03 +0100
committerNilstrieb <48135649+Nilstrieb@users.noreply.github.com>2022-12-30 11:23:01 +0100
commite15272d8d6319726b22f72ba3ccfc5275e534ca8 (patch)
tree27b8ed99fc2de15a81f444e3fc103a7f3d493731
parente5e5fcb0b758fcf7f149cc9206155dcfa18ec909 (diff)
downloadrust-e15272d8d6319726b22f72ba3ccfc5275e534ca8.tar.gz
rust-e15272d8d6319726b22f72ba3ccfc5275e534ca8.zip
Add tidy check to deny merge commits
This will prevent users with the pre-push hook from pushing a merge
commit.

Exceptions are added for subtree updates. These exceptions are a little
hacky and may be non-exhaustive but can be extended in the future.
-rw-r--r--src/tools/tidy/src/lib.rs1
-rw-r--r--src/tools/tidy/src/main.rs2
-rw-r--r--src/tools/tidy/src/no_merge.rs128
3 files changed, 131 insertions, 0 deletions
diff --git a/src/tools/tidy/src/lib.rs b/src/tools/tidy/src/lib.rs
index 698e4850bea..186cd9bb11b 100644
--- a/src/tools/tidy/src/lib.rs
+++ b/src/tools/tidy/src/lib.rs
@@ -48,6 +48,7 @@ pub mod errors;
 pub mod extdeps;
 pub mod features;
 pub mod mir_opt_tests;
+pub mod no_merge;
 pub mod pal;
 pub mod primitive_docs;
 pub mod style;
diff --git a/src/tools/tidy/src/main.rs b/src/tools/tidy/src/main.rs
index 6714c63ee62..65f0fe9743f 100644
--- a/src/tools/tidy/src/main.rs
+++ b/src/tools/tidy/src/main.rs
@@ -107,6 +107,8 @@ fn main() {
         check!(alphabetical, &compiler_path);
         check!(alphabetical, &library_path);
 
+        check!(no_merge, ());
+
         let collected = {
             drain_handles(&mut handles);
 
diff --git a/src/tools/tidy/src/no_merge.rs b/src/tools/tidy/src/no_merge.rs
new file mode 100644
index 00000000000..445004e539a
--- /dev/null
+++ b/src/tools/tidy/src/no_merge.rs
@@ -0,0 +1,128 @@
+//! This check makes sure that no accidental merge commits are introduced to the repository.
+//! It forbids all merge commits that are not caused by rollups/bors or subtree syncs.
+
+use std::process::Command;
+
+macro_rules! try_unwrap_in_ci {
+    ($expr:expr) => {
+        match $expr {
+            Ok(value) => value,
+            Err(err) if CiEnv::is_ci() => {
+                panic!("Encountered error while testing Git status: {:?}", err)
+            }
+            Err(_) => return,
+        }
+    };
+}
+
+pub fn check(_: (), bad: &mut bool) {
+    let remote = try_unwrap_in_ci!(get_rust_lang_rust_remote());
+    let merge_commits = try_unwrap_in_ci!(find_merge_commits(&remote));
+
+    let mut bad_merge_commits = merge_commits.lines().filter(|commit| {
+        !(
+            // Bors is the ruler of merge commits.
+            commit.starts_with("Auto merge of") || commit.starts_with("Rollup merge of")
+        )
+    });
+
+    if let Some(merge) = bad_merge_commits.next() {
+        tidy_error!(
+            bad,
+            "found a merge commit in the history: `{merge}`.
+To resolve the issue, see this: https://rustc-dev-guide.rust-lang.org/git.html#i-made-a-merge-commit-by-accident.
+If you're doing a subtree sync, add your tool to the list in the code that emitted this error."
+        );
+    }
+}
+
+/// 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(format!(
+            "failed to execute git config command: {}",
+            String::from_utf8_lossy(&output.stderr)
+        ));
+    }
+
+    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())
+}
+
+/// Runs `git log --merges --format=%s $REMOTE/master..HEAD` and returns all commits
+fn find_merge_commits(remote: &str) -> Result<String, String> {
+    let mut git = Command::new("git");
+    git.args([
+        "log",
+        "--merges",
+        "--format=%s",
+        &format!("{remote}/master..HEAD"),
+        // Ignore subtree syncs. Add your new subtrees here.
+        ":!src/tools/miri",
+        ":!src/tools/rust-analyzer",
+        ":!compiler/rustc_smir",
+        ":!library/portable-simd",
+        ":!compiler/rustc_codegen_gcc",
+        ":!src/tools/rustfmt",
+        ":!compiler/rustc_codegen_cranelift",
+        ":!src/tools/clippy",
+    ]);
+
+    let output = git.output().map_err(|err| format!("{err:?}"))?;
+    if !output.status.success() {
+        return Err(format!(
+            "failed to execute git log command: {}",
+            String::from_utf8_lossy(&output.stderr)
+        ));
+    }
+
+    let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?;
+
+    Ok(stdout)
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum CiEnv {
+    /// Not a CI environment.
+    None,
+    /// The Azure Pipelines environment, for Linux (including Docker), Windows, and macOS builds.
+    AzurePipelines,
+    /// The GitHub Actions environment, for Linux (including Docker), Windows and macOS builds.
+    GitHubActions,
+}
+
+impl CiEnv {
+    /// Obtains the current CI environment.
+    pub fn current() -> CiEnv {
+        if std::env::var("TF_BUILD").map_or(false, |e| e == "True") {
+            CiEnv::AzurePipelines
+        } else if std::env::var("GITHUB_ACTIONS").map_or(false, |e| e == "true") {
+            CiEnv::GitHubActions
+        } else {
+            CiEnv::None
+        }
+    }
+
+    pub fn is_ci() -> bool {
+        Self::current() != CiEnv::None
+    }
+}