From 16278a176bcf01e764d7cb66767b657d70b61956 Mon Sep 17 00:00:00 2001 From: jdx <216188+jdx@users.noreply.github.com> Date: Sun, 19 May 2024 16:54:53 -0500 Subject: [PATCH] feat: pre-commit and github action generate commands (#2144) * feat: pre-commit and github action generate commands Fixes #2115 Fixes #1692 * Commit from GitHub Actions (test) --------- Co-authored-by: mise[bot] <123107610+mise-en-dev@users.noreply.github.com> --- .node-version | 1 - docs/cli-reference.md | 61 +++++++++ e2e/forge/test_npm | 1 + man/man1/mise.1 | 3 + mise.usage.kdl | 40 ++++++ src/cli/generate/git_pre_commit.rs | 84 +++++++++++++ src/cli/generate/github_action.rs | 118 ++++++++++++++++++ src/cli/generate/mod.rs | 32 +++++ ...git_pre_commit__tests__git_pre_commit.snap | 6 + ...commit__tests__git_pre_commit_write-2.snap | 6 + ...e_commit__tests__git_pre_commit_write.snap | 5 + ...__github_action__tests__github_action.snap | 28 +++++ ..._action__tests__github_action_write-2.snap | 28 +++++ ...ub_action__tests__github_action_write.snap | 5 + src/cli/mod.rs | 3 + src/git.rs | 7 ++ src/test.rs | 26 +++- 17 files changed, 452 insertions(+), 2 deletions(-) delete mode 100644 .node-version create mode 100644 src/cli/generate/git_pre_commit.rs create mode 100644 src/cli/generate/github_action.rs create mode 100644 src/cli/generate/mod.rs create mode 100644 src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit.snap create mode 100644 src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit_write-2.snap create mode 100644 src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit_write.snap create mode 100644 src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action.snap create mode 100644 src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action_write-2.snap create mode 100644 src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action_write.snap diff --git a/.node-version b/.node-version deleted file mode 100644 index a77793ecc..000000000 --- a/.node-version +++ /dev/null @@ -1 +0,0 @@ -lts/hydrogen diff --git a/docs/cli-reference.md b/docs/cli-reference.md index cb57e60a3..1e46a5668 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -411,6 +411,67 @@ Examples: $ mise x -C /path/to/project node@20 -- node ./app.js ``` +## `mise generate git-pre-commit [OPTIONS]` + +```text +[experimental] Generate a git pre-commit hook + +This command generates a git pre-commit hook that runs a mise task like `mise run pre-commit` +when you commit changes to your repository. + +Usage: generate git-pre-commit [OPTIONS] + +Options: + --hook + Which hook to generate (saves to .git/hooks/$hook) + + [default: pre-commit] + + -t, --task + The task to run when the pre-commit hook is triggered + + [default: pre-commit] + + -w, --write + write to .git/hooks/pre-commit and make it executable + +Examples: + + $ mise generate git-pre-commit --write --task=pre-commit + $ git commit -m "feat: add new feature" # runs `mise run pre-commit` +``` + +## `mise generate github-action [OPTIONS]` + +```text +[experimental] Generate a Github Action workflow file + +This command generates a Github Action workflow file that runs a mise task like `mise run ci` +when you push changes to your repository. + +Usage: generate github-action [OPTIONS] + +Options: + -n, --name + the name of the workflow to generate + + [default: ci] + + -t, --task + The task to run when the workflow is triggered + + [default: ci] + + -w, --write + write to .github/workflows/$name.yml + +Examples: + + $ mise generate github-action --write --task=ci + $ git commit -m "feat: add new feature" + $ git push # runs `mise run ci` on Github +``` + ## `mise implode [OPTIONS]` ```text diff --git a/e2e/forge/test_npm b/e2e/forge/test_npm index 807a7140e..98d7f0106 100644 --- a/e2e/forge/test_npm +++ b/e2e/forge/test_npm @@ -3,5 +3,6 @@ require_cmd npm export NPM_CONFIG_FUND=false +mise use node assert "mise x npm:prettier@3.1.0 -- prettier -v" "3.1.0" assert "FORCE_COLOR=0 mise x npm:@antfu/ni@0.21.12 -- ni -v 2>/dev/null | head -n1" "@antfu/ni v0.21.12" diff --git a/man/man1/mise.1 b/man/man1/mise.1 index 5d38333cb..9f9f2cb95 100644 --- a/man/man1/mise.1 +++ b/man/man1/mise.1 @@ -74,6 +74,9 @@ Exports env vars to activate mise a single time mise\-exec(1) Execute a command with tool(s) set .TP +mise\-generate(1) +[experimental] Generate files for various tools/services +.TP mise\-implode(1) Removes mise CLI and all related data .TP diff --git a/mise.usage.kdl b/mise.usage.kdl index c82a6cca7..089cd4ded 100644 --- a/mise.usage.kdl +++ b/mise.usage.kdl @@ -306,6 +306,46 @@ The "--" separates runtimes from the commands to pass along to the subprocess."# arg "[TOOL@VERSION]..." help="Tool(s) to start e.g.: node@20 python@3.10" var=true arg "[COMMAND]..." help="Command string to execute (same as --command)" var=true } +cmd "generate" subcommand_required=true help="[experimental] Generate files for various tools/services" { + cmd "git-pre-commit" help="[experimental] Generate a git pre-commit hook" { + alias "pre-commit" hide=true + long_help r"[experimental] Generate a git pre-commit hook + +This command generates a git pre-commit hook that runs a mise task like `mise run pre-commit` +when you commit changes to your repository." + after_long_help r#"Examples: + + $ mise generate git-pre-commit --write --task=pre-commit + $ git commit -m "feat: add new feature" # runs `mise run pre-commit` +"# + flag "--hook" help="Which hook to generate (saves to .git/hooks/$hook)" { + arg "" + } + flag "-t --task" help="The task to run when the pre-commit hook is triggered" { + arg "" + } + flag "-w --write" help="write to .git/hooks/pre-commit and make it executable" + } + cmd "github-action" help="[experimental] Generate a Github Action workflow file" { + long_help r"[experimental] Generate a Github Action workflow file + +This command generates a Github Action workflow file that runs a mise task like `mise run ci` +when you push changes to your repository." + after_long_help r#"Examples: + + $ mise generate github-action --write --task=ci + $ git commit -m "feat: add new feature" + $ git push # runs `mise run ci` on Github +"# + flag "-n --name" help="the name of the workflow to generate" { + arg "" + } + flag "-t --task" help="The task to run when the workflow is triggered" { + arg "" + } + flag "-w --write" help="write to .github/workflows/$name.yml" + } +} cmd "global" hide=true help="Sets/gets the global tool version(s)" { alias "g" hide=true long_help r"Sets/gets the global tool version(s) diff --git a/src/cli/generate/git_pre_commit.rs b/src/cli/generate/git_pre_commit.rs new file mode 100644 index 000000000..98ea3b2b6 --- /dev/null +++ b/src/cli/generate/git_pre_commit.rs @@ -0,0 +1,84 @@ +use xx::file::display_path; + +use crate::config::Settings; +use crate::file; +use crate::git::Git; + +/// [experimental] Generate a git pre-commit hook +/// +/// This command generates a git pre-commit hook that runs a mise task like `mise run pre-commit` +/// when you commit changes to your repository. +#[derive(Debug, clap::Args)] +#[clap(verbatim_doc_comment, alias = "pre-commit", after_long_help = AFTER_LONG_HELP)] +pub struct GitPreCommit { + /// Which hook to generate (saves to .git/hooks/$hook) + #[clap(long, default_value = "pre-commit")] + hook: String, + /// The task to run when the pre-commit hook is triggered + #[clap(long, short, default_value = "pre-commit")] + task: String, + /// write to .git/hooks/pre-commit and make it executable + #[clap(long, short)] + write: bool, +} + +impl GitPreCommit { + pub fn run(self) -> eyre::Result<()> { + let settings = Settings::get(); + settings.ensure_experimental("generate git-pre-commit")?; + let output = self.generate(); + if self.write { + let path = Git::get_root()?.join(".git/hooks").join(&self.hook); + file::write(&path, &output)?; + file::make_executable(&path)?; + miseprintln!("Wrote to {}", display_path(&path)); + } else { + miseprintln!("{output}"); + } + Ok(()) + } + + fn generate(&self) -> String { + let task = &self.task; + format!( + r#"#!/bin/sh +mise run {task} +"# + ) + } +} + +static AFTER_LONG_HELP: &str = color_print::cstr!( + r#"Examples: + + $ mise generate git-pre-commit --write --task=pre-commit + $ git commit -m "feat: add new feature" # runs `mise run pre-commit` +"# +); + +#[cfg(test)] +mod tests { + use test_log::test; + + use crate::file; + use crate::git::Git; + use crate::test::{cleanup, reset, setup_git_repo}; + + #[test] + fn test_git_pre_commit() { + reset(); + setup_git_repo(); + assert_cli_snapshot!("generate", "pre-commit", "--task=testing123"); + cleanup(); + } + #[test] + fn test_git_pre_commit_write() { + reset(); + setup_git_repo(); + assert_cli_snapshot!("generate", "pre-commit", "-w", "--hook", "testing123"); + let path = Git::get_root().unwrap().join(".git/hooks/testing123"); + assert_snapshot!(file::read_to_string(&path).unwrap()); + assert!(file::is_executable(&path)); + cleanup(); + } +} diff --git a/src/cli/generate/github_action.rs b/src/cli/generate/github_action.rs new file mode 100644 index 000000000..bc412bac5 --- /dev/null +++ b/src/cli/generate/github_action.rs @@ -0,0 +1,118 @@ +use xx::file; + +use crate::config::Settings; +use crate::file::display_path; +use crate::git::Git; + +/// [experimental] Generate a Github Action workflow file +/// +/// This command generates a Github Action workflow file that runs a mise task like `mise run ci` +/// when you push changes to your repository. +#[derive(Debug, clap::Args)] +#[clap(verbatim_doc_comment, after_long_help = AFTER_LONG_HELP)] +pub struct GithubAction { + /// the name of the workflow to generate + #[clap(long, short, default_value = "ci")] + name: String, + /// The task to run when the workflow is triggered + #[clap(long, short, default_value = "ci")] + task: String, + /// write to .github/workflows/$name.yml + #[clap(long, short)] + write: bool, +} + +impl GithubAction { + pub fn run(self) -> eyre::Result<()> { + let settings = Settings::get(); + settings.ensure_experimental("generate github-action")?; + let output = self.generate()?; + if self.write { + let path = Git::get_root()? + .join(".github/workflows") + .join(format!("{}.yml", &self.name)); + file::write(&path, &output)?; + miseprintln!("Wrote to {}", display_path(&path)); + } else { + miseprintln!("{output}"); + } + Ok(()) + } + + fn generate(&self) -> eyre::Result { + let branch = Git::new(Git::get_root()?).current_branch()?; + let name = &self.name; + let task = &self.task; + Ok(format!( + r#"name: {name} + +on: + workflow_dispatch: + pull_request: + push: + tags: ["*"] + branches: ["{branch}"] + +concurrency: + group: ${{{{ github.workflow }}}}-${{{{ github.ref }}}} + cancel-in-progress: true + +env: + MISE_EXPERIMENTAL: true + +jobs: + {name}: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: jdx/mise-action@v2 + - run: mise run {task} +"# + )) + } +} + +static AFTER_LONG_HELP: &str = color_print::cstr!( + r#"Examples: + + $ mise generate github-action --write --task=ci + $ git commit -m "feat: add new feature" + $ git push # runs `mise run ci` on Github +"# +); + +#[cfg(test)] +mod tests { + use test_log::test; + + use crate::file; + use crate::git::Git; + use crate::test::{cleanup, reset, setup_git_repo}; + + #[test] + fn test_github_action() { + reset(); + setup_git_repo(); + assert_cli_snapshot!("generate", "github-action"); + cleanup(); + } + #[test] + fn test_github_action_write() { + reset(); + setup_git_repo(); + assert_cli_snapshot!( + "generate", + "github-action", + "-w", + "-ttesting123", + "-n=testing123" + ); + let path = Git::get_root() + .unwrap() + .join(".github/workflows/testing123.yml"); + let contents = file::read_to_string(path).unwrap(); + assert_snapshot!(contents); + cleanup(); + } +} diff --git a/src/cli/generate/mod.rs b/src/cli/generate/mod.rs new file mode 100644 index 000000000..fca7d6938 --- /dev/null +++ b/src/cli/generate/mod.rs @@ -0,0 +1,32 @@ +use clap::Subcommand; + +mod git_pre_commit; +mod github_action; + +/// [experimental] Generate files for various tools/services +#[derive(Debug, clap::Args)] +pub struct Generate { + #[clap(subcommand)] + command: Commands, +} + +#[derive(Debug, Subcommand)] +enum Commands { + GitPreCommit(git_pre_commit::GitPreCommit), + GithubAction(github_action::GithubAction), +} + +impl Commands { + pub fn run(self) -> eyre::Result<()> { + match self { + Self::GitPreCommit(cmd) => cmd.run(), + Self::GithubAction(cmd) => cmd.run(), + } + } +} + +impl Generate { + pub fn run(self) -> eyre::Result<()> { + self.command.run() + } +} diff --git a/src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit.snap b/src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit.snap new file mode 100644 index 000000000..f69d9a17a --- /dev/null +++ b/src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit.snap @@ -0,0 +1,6 @@ +--- +source: src/cli/generate/git_pre_commit.rs +expression: output +--- +#!/bin/sh +mise run testing123 diff --git a/src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit_write-2.snap b/src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit_write-2.snap new file mode 100644 index 000000000..bc5eaff81 --- /dev/null +++ b/src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit_write-2.snap @@ -0,0 +1,6 @@ +--- +source: src/cli/generate/git_pre_commit.rs +expression: "file::read_to_string(&path).unwrap()" +--- +#!/bin/sh +mise run pre-commit diff --git a/src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit_write.snap b/src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit_write.snap new file mode 100644 index 000000000..421431148 --- /dev/null +++ b/src/cli/generate/snapshots/mise__cli__generate__git_pre_commit__tests__git_pre_commit_write.snap @@ -0,0 +1,5 @@ +--- +source: src/cli/generate/git_pre_commit.rs +expression: output +--- +Wrote to ~/cwd/.git/hooks/testing123 diff --git a/src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action.snap b/src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action.snap new file mode 100644 index 000000000..7f8b2206e --- /dev/null +++ b/src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action.snap @@ -0,0 +1,28 @@ +--- +source: src/cli/generate/github_action.rs +expression: output +--- +name: ci + +on: + workflow_dispatch: + pull_request: + push: + tags: ["*"] + branches: ["trunk"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + MISE_EXPERIMENTAL: true + +jobs: + ci: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: jdx/mise-action@v2 + - run: mise run ci diff --git a/src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action_write-2.snap b/src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action_write-2.snap new file mode 100644 index 000000000..26b4ce79a --- /dev/null +++ b/src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action_write-2.snap @@ -0,0 +1,28 @@ +--- +source: src/cli/generate/github_action.rs +expression: contents +--- +name: testing123 + +on: + workflow_dispatch: + pull_request: + push: + tags: ["*"] + branches: ["trunk"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + MISE_EXPERIMENTAL: true + +jobs: + testing123: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: jdx/mise-action@v2 + - run: mise run testing123 diff --git a/src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action_write.snap b/src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action_write.snap new file mode 100644 index 000000000..c97ba3293 --- /dev/null +++ b/src/cli/generate/snapshots/mise__cli__generate__github_action__tests__github_action_write.snap @@ -0,0 +1,5 @@ +--- +source: src/cli/generate/github_action.rs +expression: output +--- +Wrote to ~/cwd/.github/workflows/testing123.yml diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f7535492f..1da26254b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -20,6 +20,7 @@ mod doctor; mod env; pub mod exec; mod external; +mod generate; mod global; mod hook_env; mod hook_not_found; @@ -75,6 +76,7 @@ pub enum Commands { Doctor(doctor::Doctor), Env(env::Env), Exec(exec::Exec), + Generate(generate::Generate), Global(global::Global), HookEnv(hook_env::HookEnv), HookNotFound(hook_not_found::HookNotFound), @@ -132,6 +134,7 @@ impl Commands { Self::Doctor(cmd) => cmd.run(), Self::Env(cmd) => cmd.run(), Self::Exec(cmd) => cmd.run(), + Self::Generate(cmd) => cmd.run(), Self::Global(cmd) => cmd.run(), Self::HookEnv(cmd) => cmd.run(), Self::HookNotFound(cmd) => cmd.run(), diff --git a/src/git.rs b/src/git.rs index f4003cd54..38826d630 100644 --- a/src/git.rs +++ b/src/git.rs @@ -197,6 +197,13 @@ impl Git { pub fn exists(&self) -> bool { self.dir.join(".git").is_dir() } + + pub fn get_root() -> eyre::Result { + Ok(cmd!("git", "rev-parse", "--show-toplevel") + .read()? + .trim() + .into()) + } } fn get_git_version() -> Result { diff --git a/src/test.rs b/src/test.rs index a1859dbc7..12c84844f 100644 --- a/src/test.rs +++ b/src/test.rs @@ -6,7 +6,7 @@ use color_eyre::{Help, SectionExt}; use crate::cli::Cli; use crate::config::{config_file, Config}; use crate::output::tests::{STDERR, STDOUT}; -use crate::{dirs, env, file, forge}; +use crate::{cmd, dirs, env, file, forge}; #[macro_export] macro_rules! assert_cli_snapshot { @@ -131,6 +131,30 @@ pub fn reset() { assert_cli!("install"); } +pub fn setup_git_repo() { + cmd!("git", "init", "-b", "trunk").run().unwrap(); + file::write("README.md", "# testing123").unwrap(); + cmd!("git", "add", "README.md").run().unwrap(); + cmd!( + "git", + "-c", + "user.name=ferris", + "-c", + "user.email=ferris@example.com", + "commit", + "-m", + "feat: add README" + ) + .run() + .unwrap(); +} + +pub fn cleanup() { + let _ = file::remove_all(".github"); + let _ = file::remove_all(".git"); + let _ = file::remove_all("README.md"); +} + pub fn replace_path(input: &str) -> String { let path = join_paths(&*env::PATH) .unwrap()