Skip to content

Commit a6b862f

Browse files
committed
Implement sync subcommand in clippy_dev
Now that JOSH is used to sync, it is much easier to script the sync process. This introduces the two commands `sync pull` and `sync push`. The first one will pull changes from the Rust repo, the second one will push the changes to the Rust repo. For details, see the documentation in the book.
1 parent 1ecb18a commit a6b862f

File tree

3 files changed

+250
-5
lines changed

3 files changed

+250
-5
lines changed

clippy_dev/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ edition = "2024"
77
[dependencies]
88
chrono = { version = "0.4.38", default-features = false, features = ["clock"] }
99
clap = { version = "4.4", features = ["derive"] }
10+
directories = "5"
1011
indoc = "1.0"
1112
itertools = "0.12"
1213
opener = "0.7"
1314
walkdir = "2.3"
15+
xshell = "0.2"
1416

1517
[package.metadata.rust-analyzer]
1618
# This package uses #[feature(rustc_private)]

clippy_dev/src/main.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ fn main() {
8787
),
8888
DevCommand::Deprecate { name, reason } => deprecate_lint::deprecate(clippy.version, &name, &reason),
8989
DevCommand::Sync(SyncCommand { subcommand }) => match subcommand {
90-
SyncSubcommand::UpdateNightly => sync::update_nightly(),
90+
SyncSubcommand::Pull => sync::rustc_pull(),
91+
SyncSubcommand::Push {
92+
repo_path,
93+
user,
94+
branch,
95+
force,
96+
} => sync::rustc_push(repo_path, &user, &branch, force),
9197
},
9298
DevCommand::Release(ReleaseCommand { subcommand }) => match subcommand {
9399
ReleaseSubcommand::BumpVersion => release::bump_version(clippy.version),
@@ -329,9 +335,24 @@ struct SyncCommand {
329335

330336
#[derive(Subcommand)]
331337
enum SyncSubcommand {
332-
#[command(name = "update_nightly")]
333-
/// Update nightly version in `rust-toolchain.toml` and `clippy_utils`
334-
UpdateNightly,
338+
/// Pull changes from rustc and update the toolchain
339+
Pull,
340+
/// Push changes to rustc
341+
Push {
342+
/// The path to a rustc repo that will be used for pushing changes
343+
repo_path: String,
344+
#[arg(long)]
345+
/// The GitHub username to use for pushing changes
346+
user: String,
347+
#[arg(long, short, default_value = "clippy-subtree-update")]
348+
/// The branch to push to
349+
///
350+
/// This is mostly for experimentation and usually the default should be used.
351+
branch: String,
352+
#[arg(long, short)]
353+
/// Force push changes
354+
force: bool,
355+
},
335356
}
336357

337358
#[derive(Args)]

clippy_dev/src/sync.rs

Lines changed: 223 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,101 @@
11
use crate::utils::{FileUpdater, update_text_region_fn};
22
use chrono::offset::Utc;
33
use std::fmt::Write;
4+
use std::path::Path;
5+
use std::process;
6+
use std::process::exit;
47

5-
pub fn update_nightly() {
8+
use xshell::{Shell, cmd};
9+
10+
const JOSH_FILTER: &str = ":rev(2efebd2f0c03dabbe5c3ad7b4ebfbd99238d1fb2:prefix=src/tools/clippy):/src/tools/clippy";
11+
const JOSH_PORT: &str = "42042";
12+
13+
fn start_josh() -> impl Drop {
14+
// Create a wrapper that stops it on drop.
15+
struct Josh(process::Child);
16+
impl Drop for Josh {
17+
fn drop(&mut self) {
18+
#[cfg(unix)]
19+
{
20+
// Try to gracefully shut it down.
21+
process::Command::new("kill")
22+
.args(["-s", "INT", &self.0.id().to_string()])
23+
.output()
24+
.expect("failed to SIGINT josh-proxy");
25+
// Sadly there is no "wait with timeout"... so we just give it some time to finish.
26+
std::thread::sleep(std::time::Duration::from_secs(1));
27+
// Now hopefully it is gone.
28+
if self.0.try_wait().expect("failed to wait for josh-proxy").is_some() {
29+
return;
30+
}
31+
}
32+
// If that didn't work (or we're not on Unix), kill it hard.
33+
eprintln!("I have to kill josh-proxy the hard way, let's hope this does not break anything.");
34+
self.0.kill().expect("failed to SIGKILL josh-proxy");
35+
}
36+
}
37+
38+
// Determine cache directory.
39+
let local_dir = {
40+
let user_dirs = directories::ProjectDirs::from("org", "rust-lang", "clippy-josh").unwrap();
41+
user_dirs.cache_dir().to_owned()
42+
};
43+
println!("Using local cache directory: {}", local_dir.display());
44+
45+
// Start josh, silencing its output.
46+
let mut cmd = process::Command::new("josh-proxy");
47+
cmd.arg("--local").arg(local_dir);
48+
cmd.arg("--remote").arg("https://github.com");
49+
cmd.arg("--port").arg(JOSH_PORT);
50+
cmd.arg("--no-background");
51+
cmd.stdout(process::Stdio::null());
52+
cmd.stderr(process::Stdio::null());
53+
let josh = cmd
54+
.spawn()
55+
.expect("failed to start josh-proxy, make sure it is installed");
56+
// Give it some time so hopefully the port is open.
57+
std::thread::sleep(std::time::Duration::from_secs(1));
58+
59+
Josh(josh)
60+
}
61+
62+
fn rustc_hash() -> String {
63+
let sh = Shell::new().expect("failed to create shell");
64+
// Make sure we pick up the updated toolchain (usually rustup pins the toolchain
65+
// inside a single cargo/rustc invocation via this env var).
66+
sh.set_var("RUSTUP_TOOLCHAIN", "");
67+
cmd!(sh, "rustc --version --verbose")
68+
.read()
69+
.expect("failed to run `rustc -vV`")
70+
.lines()
71+
.find(|line| line.starts_with("commit-hash:"))
72+
.expect("failed to parse `rustc -vV`")
73+
.split_whitespace()
74+
.last()
75+
.expect("failed to get commit from `rustc -vV`")
76+
.to_string()
77+
}
78+
79+
fn assert_clean_repo(sh: &Shell) {
80+
if !cmd!(sh, "git status --untracked-files=no --porcelain")
81+
.read()
82+
.expect("failed to run git status")
83+
.is_empty()
84+
{
85+
eprintln!("working directory must be clean before running `cargo dev sync pull`");
86+
exit(1);
87+
}
88+
}
89+
90+
pub fn rustc_pull() {
91+
const MERGE_COMMIT_MESSAGE: &str = "Merge from rustc";
92+
93+
let sh = Shell::new().expect("failed to create shell");
94+
sh.change_dir(clippy_project_root());
95+
96+
assert_clean_repo(&sh);
97+
98+
// Update rust-toolchain file
699
let date = Utc::now().format("%Y-%m-%d").to_string();
7100
let toolchain_update = &mut update_text_region_fn(
8101
"# begin autogenerated nightly\n",
@@ -22,4 +115,133 @@ pub fn update_nightly() {
22115
let mut updater = FileUpdater::default();
23116
updater.update_file("rust-toolchain.toml", toolchain_update);
24117
updater.update_file("clippy_utils/README.md", readme_update);
118+
119+
let message = format!("Bump nightly version -> {date}");
120+
cmd!(sh, "git commit rust-toolchain --no-verify -m {message}")
121+
.run()
122+
.expect("FAILED to commit rust-toolchain file, something went wrong");
123+
124+
let commit = rustc_hash();
125+
126+
// Make sure josh is running in this scope
127+
{
128+
let _josh = start_josh();
129+
130+
// Fetch given rustc commit.
131+
cmd!(
132+
sh,
133+
"git fetch http://localhost:{JOSH_PORT}/rust-lang/rust.git@{commit}{JOSH_FILTER}.git"
134+
)
135+
.run()
136+
.inspect_err(|_| {
137+
// Try to un-do the previous `git commit`, to leave the repo in the state we found it.
138+
cmd!(sh, "git reset --hard HEAD^")
139+
.run()
140+
.expect("FAILED to clean up again after failed `git fetch`, sorry for that");
141+
})
142+
.expect("FAILED to fetch new commits, something went wrong");
143+
}
144+
145+
// This should not add any new root commits. So count those before and after merging.
146+
let num_roots = || -> u32 {
147+
cmd!(sh, "git rev-list HEAD --max-parents=0 --count")
148+
.read()
149+
.expect("failed to determine the number of root commits")
150+
.parse::<u32>()
151+
.unwrap()
152+
};
153+
let num_roots_before = num_roots();
154+
155+
// Merge the fetched commit.
156+
cmd!(sh, "git merge FETCH_HEAD --no-verify --no-ff -m {MERGE_COMMIT_MESSAGE}")
157+
.run()
158+
.expect("FAILED to merge new commits, something went wrong");
159+
160+
// Check that the number of roots did not increase.
161+
if num_roots() != num_roots_before {
162+
eprintln!("Josh created a new root commit. This is probably not the history you want.");
163+
exit(1);
164+
}
165+
}
166+
167+
pub(crate) const PUSH_PR_DESCRIPTION: &str = "Sync from Clippy commit:";
168+
169+
pub fn rustc_push(rustc_path: String, github_user: &str, branch: &str, force: bool) {
170+
let sh = Shell::new().expect("failed to create shell");
171+
sh.change_dir(clippy_project_root());
172+
173+
assert_clean_repo(&sh);
174+
175+
// Prepare the branch. Pushing works much better if we use as base exactly
176+
// the commit that we pulled from last time, so we use the `rustc --version`
177+
// to find out which commit that would be.
178+
let base = rustc_hash();
179+
180+
println!("Preparing {github_user}/rust (base: {base})...");
181+
sh.change_dir(rustc_path);
182+
if !force
183+
&& cmd!(sh, "git fetch https://github.com/{github_user}/rust {branch}")
184+
.ignore_stderr()
185+
.read()
186+
.is_ok()
187+
{
188+
eprintln!(
189+
"The branch '{branch}' seems to already exist in 'https://github.com/{github_user}/rust'. Please delete it and try again."
190+
);
191+
exit(1);
192+
}
193+
cmd!(sh, "git fetch https://github.com/rust-lang/rust {base}")
194+
.run()
195+
.expect("failed to fetch base commit");
196+
let force_flag = if force { "--force" } else { "" };
197+
cmd!(
198+
sh,
199+
"git push https://github.com/{github_user}/rust {base}:refs/heads/{branch} {force_flag}"
200+
)
201+
.ignore_stdout()
202+
.ignore_stderr() // silence the "create GitHub PR" message
203+
.run()
204+
.expect("failed to push base commit to the new branch");
205+
206+
// Make sure josh is running in this scope
207+
{
208+
let _josh = start_josh();
209+
210+
// Do the actual push.
211+
sh.change_dir(clippy_project_root());
212+
println!("Pushing Clippy changes...");
213+
cmd!(
214+
sh,
215+
"git push http://localhost:{JOSH_PORT}/{github_user}/rust.git{JOSH_FILTER}.git HEAD:{branch}"
216+
)
217+
.run()
218+
.expect("failed to push changes to Josh");
219+
220+
// Do a round-trip check to make sure the push worked as expected.
221+
cmd!(
222+
sh,
223+
"git fetch http://localhost:{JOSH_PORT}/{github_user}/rust.git{JOSH_FILTER}.git {branch}"
224+
)
225+
.ignore_stderr()
226+
.read()
227+
.expect("failed to fetch the branch from Josh");
228+
}
229+
230+
let head = cmd!(sh, "git rev-parse HEAD")
231+
.read()
232+
.expect("failed to get HEAD commit");
233+
let fetch_head = cmd!(sh, "git rev-parse FETCH_HEAD")
234+
.read()
235+
.expect("failed to get FETCH_HEAD");
236+
if head != fetch_head {
237+
eprintln!("Josh created a non-roundtrip push! Do NOT merge this into rustc!");
238+
exit(1);
239+
}
240+
println!("Confirmed that the push round-trips back to Clippy properly. Please create a rustc PR:");
241+
let description = format!("{}+rust-lang/rust-clippy@{head}", PUSH_PR_DESCRIPTION.replace(' ', "+"));
242+
println!(
243+
// Open PR with `subtree update` title to silence the `no-merges` triagebot check
244+
// See https://github.com/rust-lang/rust/pull/114157
245+
" https://github.com/rust-lang/rust/compare/{github_user}:{branch}?quick_pull=1&title=Clippy+subtree+update&body=r?+@ghost%0A%0A{description}"
246+
);
25247
}

0 commit comments

Comments
 (0)