Skip to content

Commit e15272d

Browse files
committed
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.
1 parent e5e5fcb commit e15272d

File tree

3 files changed

+131
-0
lines changed

3 files changed

+131
-0
lines changed

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

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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+
macro_rules! try_unwrap_in_ci {
7+
($expr:expr) => {
8+
match $expr {
9+
Ok(value) => value,
10+
Err(err) if CiEnv::is_ci() => {
11+
panic!("Encountered error while testing Git status: {:?}", err)
12+
}
13+
Err(_) => return,
14+
}
15+
};
16+
}
17+
18+
pub fn check(_: (), bad: &mut bool) {
19+
let remote = try_unwrap_in_ci!(get_rust_lang_rust_remote());
20+
let merge_commits = try_unwrap_in_ci!(find_merge_commits(&remote));
21+
22+
let mut bad_merge_commits = merge_commits.lines().filter(|commit| {
23+
!(
24+
// Bors is the ruler of merge commits.
25+
commit.starts_with("Auto merge of") || commit.starts_with("Rollup merge of")
26+
)
27+
});
28+
29+
if let Some(merge) = bad_merge_commits.next() {
30+
tidy_error!(
31+
bad,
32+
"found a merge commit in the history: `{merge}`.
33+
To resolve the issue, see this: https://rustc-dev-guide.rust-lang.org/git.html#i-made-a-merge-commit-by-accident.
34+
If you're doing a subtree sync, add your tool to the list in the code that emitted this error."
35+
);
36+
}
37+
}
38+
39+
/// Finds the remote for rust-lang/rust.
40+
/// For example for these remotes it will return `upstream`.
41+
/// ```text
42+
/// origin https://github.com/Nilstrieb/rust.git (fetch)
43+
/// origin https://github.com/Nilstrieb/rust.git (push)
44+
/// upstream https://github.com/rust-lang/rust (fetch)
45+
/// upstream https://github.com/rust-lang/rust (push)
46+
/// ```
47+
fn get_rust_lang_rust_remote() -> Result<String, String> {
48+
let mut git = Command::new("git");
49+
git.args(["config", "--local", "--get-regex", "remote\\..*\\.url"]);
50+
51+
let output = git.output().map_err(|err| format!("{err:?}"))?;
52+
if !output.status.success() {
53+
return Err(format!(
54+
"failed to execute git config command: {}",
55+
String::from_utf8_lossy(&output.stderr)
56+
));
57+
}
58+
59+
let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?;
60+
61+
let rust_lang_remote = stdout
62+
.lines()
63+
.find(|remote| remote.contains("rust-lang"))
64+
.ok_or_else(|| "rust-lang/rust remote not found".to_owned())?;
65+
66+
let remote_name =
67+
rust_lang_remote.split('.').nth(1).ok_or_else(|| "remote name not found".to_owned())?;
68+
Ok(remote_name.into())
69+
}
70+
71+
/// Runs `git log --merges --format=%s $REMOTE/master..HEAD` and returns all commits
72+
fn find_merge_commits(remote: &str) -> Result<String, String> {
73+
let mut git = Command::new("git");
74+
git.args([
75+
"log",
76+
"--merges",
77+
"--format=%s",
78+
&format!("{remote}/master..HEAD"),
79+
// Ignore subtree syncs. Add your new subtrees here.
80+
":!src/tools/miri",
81+
":!src/tools/rust-analyzer",
82+
":!compiler/rustc_smir",
83+
":!library/portable-simd",
84+
":!compiler/rustc_codegen_gcc",
85+
":!src/tools/rustfmt",
86+
":!compiler/rustc_codegen_cranelift",
87+
":!src/tools/clippy",
88+
]);
89+
90+
let output = git.output().map_err(|err| format!("{err:?}"))?;
91+
if !output.status.success() {
92+
return Err(format!(
93+
"failed to execute git log command: {}",
94+
String::from_utf8_lossy(&output.stderr)
95+
));
96+
}
97+
98+
let stdout = String::from_utf8(output.stdout).map_err(|err| format!("{err:?}"))?;
99+
100+
Ok(stdout)
101+
}
102+
103+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
104+
pub enum CiEnv {
105+
/// Not a CI environment.
106+
None,
107+
/// The Azure Pipelines environment, for Linux (including Docker), Windows, and macOS builds.
108+
AzurePipelines,
109+
/// The GitHub Actions environment, for Linux (including Docker), Windows and macOS builds.
110+
GitHubActions,
111+
}
112+
113+
impl CiEnv {
114+
/// Obtains the current CI environment.
115+
pub fn current() -> CiEnv {
116+
if std::env::var("TF_BUILD").map_or(false, |e| e == "True") {
117+
CiEnv::AzurePipelines
118+
} else if std::env::var("GITHUB_ACTIONS").map_or(false, |e| e == "true") {
119+
CiEnv::GitHubActions
120+
} else {
121+
CiEnv::None
122+
}
123+
}
124+
125+
pub fn is_ci() -> bool {
126+
Self::current() != CiEnv::None
127+
}
128+
}

0 commit comments

Comments
 (0)