Skip to content

Commit 4839886

Browse files
committedDec 30, 2022
Auto merge of rust-lang#105058 - Nilstrieb:no-merge-commits-for-you-only-bors-is-allowed-to-do-that, r=jyn514
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. I added a link to `@jyn514's` blog post for the error case because that's the best resource to solve merge commits. But it would probably be better if it was integrated into https://rustc-dev-guide.rust-lang.org/git.html#no-merge-policy, then we could link that instead. r? `@jyn514`
2 parents ce85c98 + 878af66 commit 4839886

File tree

18 files changed

+195
-60
lines changed

18 files changed

+195
-60
lines changed
 

‎.github/workflows/ci.yml

+15
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ jobs:
7171
uses: actions/checkout@v3
7272
with:
7373
fetch-depth: 2
74+
- name: "checkout the `master` branch for tidy"
75+
uses: actions/checkout@v3
76+
with:
77+
ref: master
78+
fetch-depth: 1
7479
- name: configure the PR in which the error message will be posted
7580
run: "echo \"[CI_PR_NUMBER=$num]\""
7681
env:
@@ -485,6 +490,11 @@ jobs:
485490
uses: actions/checkout@v3
486491
with:
487492
fetch-depth: 2
493+
- name: "checkout the `master` branch for tidy"
494+
uses: actions/checkout@v3
495+
with:
496+
ref: master
497+
fetch-depth: 1
488498
- name: configure the PR in which the error message will be posted
489499
run: "echo \"[CI_PR_NUMBER=$num]\""
490500
env:
@@ -600,6 +610,11 @@ jobs:
600610
uses: actions/checkout@v3
601611
with:
602612
fetch-depth: 2
613+
- name: "checkout the `master` branch for tidy"
614+
uses: actions/checkout@v3
615+
with:
616+
ref: master
617+
fetch-depth: 1
603618
- name: configure the PR in which the error message will be posted
604619
run: "echo \"[CI_PR_NUMBER=$num]\""
605620
env:

‎Cargo.lock

+5
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ dependencies = [
259259
"toml",
260260
]
261261

262+
[[package]]
263+
name = "build_helper"
264+
version = "0.1.0"
265+
262266
[[package]]
263267
name = "bump-stage0"
264268
version = "0.1.0"
@@ -5304,6 +5308,7 @@ dependencies = [
53045308
name = "tidy"
53055309
version = "0.1.0"
53065310
dependencies = [
5311+
"build_helper",
53075312
"cargo_metadata 0.14.0",
53085313
"ignore",
53095314
"lazy_static",

‎Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ members = [
44
"library/std",
55
"library/test",
66
"src/rustdoc-json-types",
7+
"src/tools/build_helper",
78
"src/tools/cargotest",
89
"src/tools/clippy",
910
"src/tools/clippy/clippy_dev",

‎src/bootstrap/Cargo.lock

+5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ dependencies = [
3636
name = "bootstrap"
3737
version = "0.0.0"
3838
dependencies = [
39+
"build_helper",
3940
"cc",
4041
"cmake",
4142
"fd-lock",
@@ -70,6 +71,10 @@ dependencies = [
7071
"regex-automata",
7172
]
7273

74+
[[package]]
75+
name = "build_helper"
76+
version = "0.1.0"
77+
7378
[[package]]
7479
name = "cc"
7580
version = "1.0.73"

‎src/bootstrap/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ path = "bin/sccache-plus-cl.rs"
3030
test = false
3131

3232
[dependencies]
33+
build_helper = { path = "../tools/build_helper" }
3334
cmake = "0.1.38"
3435
fd-lock = "3.0.8"
3536
filetime = "0.2"

‎src/bootstrap/format.rs

+1-29
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use crate::builder::Builder;
44
use crate::util::{output, program_out_of_date, t};
5+
use build_helper::git::get_rust_lang_rust_remote;
56
use ignore::WalkBuilder;
67
use std::collections::VecDeque;
78
use std::path::{Path, PathBuf};
@@ -100,35 +101,6 @@ fn get_modified_rs_files(build: &Builder<'_>) -> Option<Vec<String>> {
100101
)
101102
}
102103

103-
/// Finds the remote for rust-lang/rust.
104-
/// For example for these remotes it will return `upstream`.
105-
/// ```text
106-
/// origin https://github.com/Nilstrieb/rust.git (fetch)
107-
/// origin https://github.com/Nilstrieb/rust.git (push)
108-
/// upstream https://github.com/rust-lang/rust (fetch)
109-
/// upstream https://github.com/rust-lang/rust (push)
110-
/// ```
111-
fn get_rust_lang_rust_remote() -> Result<String, String> {
112-
let mut git = Command::new("git");
113-
git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]);
114-
115-
let output = git.output().map_err(|err| format!("{err:?}"))?;
116-
if !output.status.success() {
117-
return Err("failed to execute git config command".to_owned());
118-
}
119-
120-
let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?;
121-
122-
let rust_lang_remote = stdout
123-
.lines()
124-
.find(|remote| remote.contains("rust-lang"))
125-
.ok_or_else(|| "rust-lang/rust remote not found".to_owned())?;
126-
127-
let remote_name =
128-
rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?;
129-
Ok(remote_name.into())
130-
}
131-
132104
#[derive(serde::Deserialize)]
133105
struct RustfmtConfig {
134106
ignore: Vec<String>,

‎src/bootstrap/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ use std::path::{Path, PathBuf};
113113
use std::process::Command;
114114
use std::str;
115115

116+
use build_helper::ci::CiEnv;
116117
use channel::GitInfo;
117118
use config::{DryRun, Target};
118119
use filetime::FileTime;
@@ -121,7 +122,7 @@ use once_cell::sync::OnceCell;
121122
use crate::builder::Kind;
122123
use crate::config::{LlvmLibunwind, TargetSelection};
123124
use crate::util::{
124-
exe, libdir, mtime, output, run, run_suppressed, symlink_dir, try_run_suppressed, CiEnv,
125+
exe, libdir, mtime, output, run, run_suppressed, symlink_dir, try_run_suppressed,
125126
};
126127

127128
mod bolt;

‎src/bootstrap/native.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ use crate::util::get_clang_cl_resource_dir;
2424
use crate::util::{self, exe, output, t, up_to_date};
2525
use crate::{CLang, GitRepo};
2626

27+
use build_helper::ci::CiEnv;
28+
2729
#[derive(Clone)]
2830
pub struct LlvmResult {
2931
/// Path to llvm-config binary.
@@ -217,7 +219,7 @@ pub(crate) fn is_ci_llvm_available(config: &Config, asserts: bool) -> bool {
217219
return false;
218220
}
219221

220-
if crate::util::CiEnv::is_ci() {
222+
if CiEnv::is_ci() {
221223
// We assume we have access to git, so it's okay to unconditionally pass
222224
// `true` here.
223225
let llvm_sha = detect_llvm_sha(config, true);

‎src/bootstrap/util.rs

-29
Original file line numberDiff line numberDiff line change
@@ -255,35 +255,6 @@ pub enum CiEnv {
255255
GitHubActions,
256256
}
257257

258-
impl CiEnv {
259-
/// Obtains the current CI environment.
260-
pub fn current() -> CiEnv {
261-
if env::var("TF_BUILD").map_or(false, |e| e == "True") {
262-
CiEnv::AzurePipelines
263-
} else if env::var("GITHUB_ACTIONS").map_or(false, |e| e == "true") {
264-
CiEnv::GitHubActions
265-
} else {
266-
CiEnv::None
267-
}
268-
}
269-
270-
pub fn is_ci() -> bool {
271-
Self::current() != CiEnv::None
272-
}
273-
274-
/// If in a CI environment, forces the command to run with colors.
275-
pub fn force_coloring_in_ci(self, cmd: &mut Command) {
276-
if self != CiEnv::None {
277-
// Due to use of stamp/docker, the output stream of rustbuild is not
278-
// a TTY in CI, so coloring is by-default turned off.
279-
// The explicit `TERM=xterm` environment is needed for
280-
// `--color always` to actually work. This env var was lost when
281-
// compiling through the Makefile. Very strange.
282-
cmd.env("TERM", "xterm").args(&["--color", "always"]);
283-
}
284-
}
285-
}
286-
287258
pub fn forcing_clang_based_tests() -> bool {
288259
if let Some(var) = env::var_os("RUSTBUILD_FORCE_CLANG_BASED_TESTS") {
289260
match &var.to_string_lossy().to_lowercase()[..] {

‎src/ci/github-actions/ci.yml

+6
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ x--expand-yaml-anchors--remove:
103103
with:
104104
fetch-depth: 2
105105

106+
- name: checkout the `master` branch for tidy
107+
uses: actions/checkout@v3
108+
with:
109+
ref: master
110+
fetch-depth: 1
111+
106112
# Rust Log Analyzer can't currently detect the PR number of a GitHub
107113
# Actions build on its own, so a hint in the log message is needed to
108114
# point it in the right direction.

‎src/tools/build_helper/Cargo.toml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[package]
2+
name = "build_helper"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]

‎src/tools/build_helper/src/ci.rs

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use std::process::Command;
2+
3+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
4+
pub enum CiEnv {
5+
/// Not a CI environment.
6+
None,
7+
/// The Azure Pipelines environment, for Linux (including Docker), Windows, and macOS builds.
8+
AzurePipelines,
9+
/// The GitHub Actions environment, for Linux (including Docker), Windows and macOS builds.
10+
GitHubActions,
11+
}
12+
13+
impl CiEnv {
14+
/// Obtains the current CI environment.
15+
pub fn current() -> CiEnv {
16+
if std::env::var("TF_BUILD").map_or(false, |e| e == "True") {
17+
CiEnv::AzurePipelines
18+
} else if std::env::var("GITHUB_ACTIONS").map_or(false, |e| e == "true") {
19+
CiEnv::GitHubActions
20+
} else {
21+
CiEnv::None
22+
}
23+
}
24+
25+
pub fn is_ci() -> bool {
26+
Self::current() != CiEnv::None
27+
}
28+
29+
/// If in a CI environment, forces the command to run with colors.
30+
pub fn force_coloring_in_ci(self, cmd: &mut Command) {
31+
if self != CiEnv::None {
32+
// Due to use of stamp/docker, the output stream of rustbuild is not
33+
// a TTY in CI, so coloring is by-default turned off.
34+
// The explicit `TERM=xterm` environment is needed for
35+
// `--color always` to actually work. This env var was lost when
36+
// compiling through the Makefile. Very strange.
37+
cmd.env("TERM", "xterm").args(&["--color", "always"]);
38+
}
39+
}
40+
}

‎src/tools/build_helper/src/git.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use std::process::Command;
2+
3+
/// Finds the remote for rust-lang/rust.
4+
/// For example for these remotes it will return `upstream`.
5+
/// ```text
6+
/// origin https://github.com/Nilstrieb/rust.git (fetch)
7+
/// origin https://github.com/Nilstrieb/rust.git (push)
8+
/// upstream https://github.com/rust-lang/rust (fetch)
9+
/// upstream https://github.com/rust-lang/rust (push)
10+
/// ```
11+
pub fn get_rust_lang_rust_remote() -> Result<String, String> {
12+
let mut git = Command::new("git");
13+
git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]);
14+
15+
let output = git.output().map_err(|err| format!("{err:?}"))?;
16+
if !output.status.success() {
17+
return Err("failed to execute git config command".to_owned());
18+
}
19+
20+
let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?;
21+
22+
let rust_lang_remote = stdout
23+
.lines()
24+
.find(|remote| remote.contains("rust-lang"))
25+
.ok_or_else(|| "rust-lang/rust remote not found".to_owned())?;
26+
27+
let remote_name =
28+
rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?;
29+
Ok(remote_name.into())
30+
}

‎src/tools/build_helper/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub mod ci;
2+
pub mod git;

‎src/tools/tidy/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ edition = "2021"
55
autobins = false
66

77
[dependencies]
8+
build_helper = { path = "../build_helper" }
89
cargo_metadata = "0.14"
910
regex = "1"
1011
miropt-test-tools = { path = "../miropt-test-tools" }

‎src/tools/tidy/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub mod errors;
4848
pub mod extdeps;
4949
pub mod features;
5050
pub mod mir_opt_tests;
51+
pub mod no_merge;
5152
pub mod pal;
5253
pub mod primitive_docs;
5354
pub mod style;

‎src/tools/tidy/src/main.rs

+2
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ fn main() {
107107
check!(alphabetical, &compiler_path);
108108
check!(alphabetical, &library_path);
109109

110+
check!(no_merge, ());
111+
110112
let collected = {
111113
drain_handles(&mut handles);
112114

‎src/tools/tidy/src/no_merge.rs

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//! This check makes sure that no accidental merge commits are introduced to the repository.
2+
//! It forbids all merge commits that are not caused by rollups/bors or subtree syncs.
3+
4+
use std::process::Command;
5+
6+
use build_helper::ci::CiEnv;
7+
use build_helper::git::get_rust_lang_rust_remote;
8+
9+
macro_rules! try_unwrap_in_ci {
10+
($expr:expr) => {
11+
match $expr {
12+
Ok(value) => value,
13+
Err(err) if CiEnv::is_ci() => {
14+
panic!("Encountered error while testing Git status: {:?}", err)
15+
}
16+
Err(_) => return,
17+
}
18+
};
19+
}
20+
21+
pub fn check(_: (), bad: &mut bool) {
22+
let remote = try_unwrap_in_ci!(get_rust_lang_rust_remote());
23+
let merge_commits = try_unwrap_in_ci!(find_merge_commits(&remote));
24+
25+
let mut bad_merge_commits = merge_commits.lines().filter(|commit| {
26+
!(
27+
// Bors is the ruler of merge commits.
28+
commit.starts_with("Auto merge of") || commit.starts_with("Rollup merge of")
29+
)
30+
});
31+
32+
if let Some(merge) = bad_merge_commits.next() {
33+
tidy_error!(
34+
bad,
35+
"found a merge commit in the history: `{merge}`.
36+
To resolve the issue, see this: https://rustc-dev-guide.rust-lang.org/git.html#i-made-a-merge-commit-by-accident.
37+
If you're doing a subtree sync, add your tool to the list in the code that emitted this error."
38+
);
39+
}
40+
}
41+
42+
/// Runs `git log --merges --format=%s $REMOTE/master..HEAD` and returns all commits
43+
fn find_merge_commits(remote: &str) -> Result<String, String> {
44+
let mut git = Command::new("git");
45+
git.args([
46+
"log",
47+
"--merges",
48+
"--format=%s",
49+
&format!("{remote}/master..HEAD"),
50+
// Ignore subtree syncs. Add your new subtrees here.
51+
":!src/tools/miri",
52+
":!src/tools/rust-analyzer",
53+
":!compiler/rustc_smir",
54+
":!library/portable-simd",
55+
":!compiler/rustc_codegen_gcc",
56+
":!src/tools/rustfmt",
57+
":!compiler/rustc_codegen_cranelift",
58+
":!src/tools/clippy",
59+
]);
60+
61+
let output = git.output().map_err(|err| format!("{err:?}"))?;
62+
if !output.status.success() {
63+
return Err(format!(
64+
"failed to execute git log command: {}",
65+
String::from_utf8_lossy(&output.stderr)
66+
));
67+
}
68+
69+
let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?;
70+
71+
Ok(stdout)
72+
}

0 commit comments

Comments
 (0)
Please sign in to comment.