diff --git a/src/bootstrap/src/core/build_steps/setup.rs b/src/bootstrap/src/core/build_steps/setup.rs index 7dfd566a58ccb..c93459bb74ec2 100644 --- a/src/bootstrap/src/core/build_steps/setup.rs +++ b/src/bootstrap/src/core/build_steps/setup.rs @@ -484,6 +484,52 @@ impl Step for Hook { } } +/// It handles Git hook setup +#[derive(Clone, Debug, Eq, PartialEq)] +enum GitPrePushHookKind { + Tidy, + TidyWithSpellcheck, +} + +impl GitPrePushHookKind { + fn prompt_user() -> io::Result> { + let prompt_str = "Available options: +1. Set tidy as pre-push hook +2. Set tidy with spellcheck as pre-push hook + +Please select [default: None]:"; + + let mut input = String::new(); + loop { + print!("{prompt_str}"); + io::stdout().flush()?; + io::stdin().read_line(&mut input)?; + + let mut modified_input = input.to_lowercase(); + modified_input.retain(|ch| !ch.is_whitespace()); + + match modified_input.as_str() { + "1" => return Ok(Some(GitPrePushHookKind::Tidy)), + "2" => return Ok(Some(GitPrePushHookKind::TidyWithSpellcheck)), + "" | "none" => return Ok(None), + _ => { + eprintln!("ERROR: unrecognized option '{}'", input.trim()); + eprintln!("NOTE: press Ctrl+C to exit"); + } + } + + input.clear(); + } + } + + fn settings_path(&self) -> PathBuf { + PathBuf::new().join("src").join("etc").join(match self { + GitPrePushHookKind::Tidy => "pre-push.sh", + GitPrePushHookKind::TidyWithSpellcheck => "pre-push-spellcheck.sh", + }) + } +} + // install a git hook to automatically run tidy, if they want fn install_git_hook_maybe(builder: &Builder<'_>, config: &Config) -> io::Result<()> { let git = helpers::git(Some(&config.src)) @@ -493,37 +539,51 @@ fn install_git_hook_maybe(builder: &Builder<'_>, config: &Config) -> io::Result< let git = PathBuf::from(git.trim()); let hooks_dir = git.join("hooks"); let dst = hooks_dir.join("pre-push"); - if dst.exists() { - // The git hook has already been set up, or the user already has a custom hook. - return Ok(()); - } println!( "\nRust's CI will automatically fail if it doesn't pass `tidy`, the internal tool for ensuring code quality. If you'd like, x.py can install a git hook for you that will automatically run `test tidy` before pushing your code to ensure your code is up to par. If you decide later that this behavior is -undesirable, simply delete the `pre-push` file from .git/hooks." +undesirable, simply delete the `pre-push` file from .git/hooks. +You have two choices of hooks, the first just runs `test tidy`, the second runs the tidy command with spellcheck. +Since the spellcheck will be installed if the binary doesn't exist under `build/`, we'll recommend you to choose the first one if you frequently clean up the build directory. +It overrides the existing pre-push hook if you already have." ); - if prompt_user("Would you like to install the git hook?: [y/N]")? != Some(PromptResult::Yes) { - println!("Ok, skipping installation!"); - return Ok(()); - } + let src = match GitPrePushHookKind::prompt_user() { + Ok(git_hook_kind) => { + if let Some(git_hook_kind) = git_hook_kind { + git_hook_kind.settings_path() + } else { + println!("Skip setting pre-push hook"); + return Ok(()); + } + } + Err(e) => { + eprintln!("ERROR: could not determine pre push hook: {e}"); + return Err(e); + } + }; + if !hooks_dir.exists() { // We need to (try to) create the hooks directory first. let _ = fs::create_dir(hooks_dir); } - let src = config.src.join("src").join("etc").join("pre-push.sh"); - match fs::hard_link(src, &dst) { + + if let Ok(true) = fs::exists(&dst) { + // Remove the existing pre-push file. + if let Err(e) = fs::remove_file(&dst) { + eprintln!("ERROR: could not remove the existing hook\n{}", e); + return Err(e); + } + } + + match fs::hard_link(config.src.join(&src), &dst) { Err(e) => { - eprintln!( - "ERROR: could not create hook {}: do you already have the git hook installed?\n{}", - dst.display(), - e - ); + eprintln!("ERROR: could not create hook {}:\n{}", dst.display(), e); return Err(e); } - Ok(_) => println!("Linked `src/etc/pre-push.sh` to `.git/hooks/pre-push`"), + Ok(_) => println!("Linked `{}` to `{}`", src.display(), dst.display()), }; Ok(()) } diff --git a/src/etc/pre-push-spellcheck.sh b/src/etc/pre-push-spellcheck.sh new file mode 100755 index 0000000000000..b7bda5d309730 --- /dev/null +++ b/src/etc/pre-push-spellcheck.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# +# Call `tidy` before git push +# Copy this script to .git/hooks to activate, +# and remove it from .git/hooks to deactivate. +# + +set -Euo pipefail + +# Check if the push is doing anything other than deleting remote branches +SKIP=true +while read LOCAL_REF LOCAL_SHA REMOTE_REF REMOTE_SHA; do + if [[ "$LOCAL_REF" != "(delete)" || \ + "$LOCAL_SHA" != "0000000000000000000000000000000000000000" ]]; then + SKIP=false + fi +done + +if $SKIP; then + echo "Skipping tidy check for branch deletion" + exit 0 +fi + +ROOT_DIR="$(git rev-parse --show-toplevel)" + +echo "Running pre-push script $ROOT_DIR/x test tidy" + +cd "$ROOT_DIR" +# The env var is necessary for printing diffs in py (fmt/lint) and cpp. +TIDY_PRINT_DIFF=1 ./x test tidy \ + --set build.locked-deps=true \ + --extra-checks auto:py,auto:cpp,auto:js,spellcheck +if [ $? -ne 0 ]; then + echo "You may use \`git push --no-verify\` to skip this check." + exit 1 +fi