diff --git a/docs/index.md b/docs/index.md index d869c6f..7118052 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,6 +12,7 @@ - [`gm status`](nu-git-manager/gm-status.md) - [`gm update-cache`](nu-git-manager/gm-update-cache.md) - [`gm report`](nu-git-manager-sugar/extra/gm-report.md) +- [`gm repo bisect`](nu-git-manager-sugar/git/gm-repo-bisect.md) - [`gm repo branch interactive-delete`](nu-git-manager-sugar/git/gm-repo-branch-interactive-delete.md) - [`gm repo branch wipe`](nu-git-manager-sugar/git/gm-repo-branch-wipe.md) - [`gm repo branches`](nu-git-manager-sugar/git/gm-repo-branches.md) diff --git a/docs/nu-git-manager-sugar/git/gm-repo-bisect.md b/docs/nu-git-manager-sugar/git/gm-repo-bisect.md new file mode 100644 index 0000000..f4e561e --- /dev/null +++ b/docs/nu-git-manager-sugar/git/gm-repo-bisect.md @@ -0,0 +1,48 @@ +# `gm repo bisect` (`nu-git-manager-sugar git`) +bisect a worktree by running a piece of code repeatedly + +# Examples +```nushell +# find a bug that was introduced in Nushell in `nushell/nushell +gm repo bisect --good 0.89.0 --bad 4458aae { + cargo run -- -n -c "def foo [x: list] { $x }; foo []" +} +``` +``` +724818030dd1de392c54788eab5030074d694ecd +``` +--- +```nushell +# avoid running the test twice more if it is expensive and you're sure +# `--good` and `--bad` are indeed "good" and "bad" +gm repo bisect --good $good --bad $bad --no-check $test +``` + +## Parameters +- parameter_name: test +- parameter_type: positional +- syntax_shape: closure() +- is_optional: false +- description: the code to run to check a given revision, should return a non-zero exit code for bad revisions +--- +- parameter_name: good +- parameter_type: named +- syntax_shape: string +- is_optional: true +- description: the initial known "good" revision +--- +- parameter_name: bad +- parameter_type: named +- syntax_shape: string +- is_optional: true +- description: the initial known "bad" revision +--- +- parameter_name: no-check +- parameter_type: switch +- is_optional: true +- description: don't check if `--good` and `--bad` are indeed "good" and "bad" + +## Signatures +| input | output | +| --------- | -------- | +| `nothing` | `string` | diff --git a/docs/nu-git-manager-sugar/git/index.md b/docs/nu-git-manager-sugar/git/index.md index 2743a3b..8cf7ca4 100644 --- a/docs/nu-git-manager-sugar/git/index.md +++ b/docs/nu-git-manager-sugar/git/index.md @@ -3,6 +3,7 @@ ## Commands +- [`gm repo bisect`](gm-repo-bisect.md) - [`gm repo branch interactive-delete`](gm-repo-branch-interactive-delete.md) - [`gm repo branch wipe`](gm-repo-branch-wipe.md) - [`gm repo branches`](gm-repo-branches.md) diff --git a/pkgs/nu-git-manager-sugar/nu-git-manager-sugar/git/mod.nu b/pkgs/nu-git-manager-sugar/nu-git-manager-sugar/git/mod.nu index 3ba82e6..601a90c 100644 --- a/pkgs/nu-git-manager-sugar/nu-git-manager-sugar/git/mod.nu +++ b/pkgs/nu-git-manager-sugar/nu-git-manager-sugar/git/mod.nu @@ -345,3 +345,122 @@ export def "gm repo query" [table: string@git-query-tables]: nothing -> table { query git $"select * from ($table)" } } + +# NOTE: would be cool to use the `throw-error` from `nu-git-manager` +def throw-error [ + error: record> +]: nothing -> error { + error make { + msg: $"(ansi red_bold)($error.msg)(ansi reset)", + label: { + text: $error.text, + span: $error.span, + }, + } +} + +# bisect a worktree by running a piece of code repeatedly +# +# # Examples +# ```nushell +# # find a bug that was introduced in Nushell in `nushell/nushell +# gm repo bisect --good 0.89.0 --bad 4458aae { +# cargo run -- -n -c "def foo [x: list] { $x }; foo []" +# } +# ``` +# ``` +# 724818030dd1de392c54788eab5030074d694ecd +# ``` +# --- +# ```nushell +# # avoid running the test twice more if it is expensive and you're sure +# # `--good` and `--bad` are indeed "good" and "bad" +# gm repo bisect --good $good --bad $bad --no-check $test +# ``` +export def "gm repo bisect" [ + test: closure, # the code to run to check a given revision, should return a non-zero exit code for bad revisions + --good: string, # the initial known "good" revision + --bad: string, # the initial known "bad" revision + --no-check, # don't check if `--good` and `--bad` are indeed "good" and "bad" +]: nothing -> string { + let res = ^git rev-parse $good | complete + if $res.exit_code != 0 { + throw-error { + msg: "invalid_git_revision", + text: $"not a valid revision in current repository", + span: (metadata $good).span, + } + } + + let res = ^git rev-parse $bad | complete + if $res.exit_code != 0 { + throw-error { + msg: "invalid_git_revision", + text: "not a valid revision in current repository", + span: (metadata $bad).span, + } + } + + if not $no_check { + print $"checking that ($good) is good..." + ^git checkout $good + try { + do $test + } catch { + throw-error { + msg: "invalid_good_revision", + text: "not a good revision", + span: (metadata $good).span, + } + } + + print $"checking that ($bad) is bad..." + ^git checkout $bad + let res = try { + do $test + true + } catch { + false + } + if $res { + throw-error { + msg: "invalid_bad_revision", + text: "not a bad revision", + span: (metadata $bad).span, + } + } + } + + ^git bisect start + ^git bisect good $good + ^git bisect bad $bad + + print $"starting bisecting at (^git rev-parse HEAD)" + + mut first_bad = "" + while $first_bad == "" { + let head = try { + do $test + "good" + } catch { + "bad" + } + + let res = ^git bisect $head + let done = $res + | lines + | get 0 + | parse "{hash} is the first bad commit" + | into record + | get hash? + if $done != null { + $first_bad = $done + } else { + print $res + } + } + + ^git bisect reset + + $first_bad +} diff --git a/pkgs/nu-git-manager-sugar/tests/mod.nu b/pkgs/nu-git-manager-sugar/tests/mod.nu index cfe8a2d..e44c0ca 100644 --- a/pkgs/nu-git-manager-sugar/tests/mod.nu +++ b/pkgs/nu-git-manager-sugar/tests/mod.nu @@ -14,6 +14,7 @@ export module imports { export def git [] { assert imports $MODULE "git" [ + "gm repo bisect", "gm repo branch interactive-delete", "gm repo branch wipe", "gm repo branches",