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