From 2e4bdbb043f1abe7dfab03f67621ea017a972b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20N=C3=BCtzi?= Date: Sun, 2 Jul 2023 19:56:56 +0200 Subject: [PATCH] feat: Simplify installer :anchor: (#121) :anchor: - The installer always updated to the latest version without beeing asked. That behavior was never really described and therefore was removed and an explicit `--update` flag is added which will keep this behavior. - A quick install script `scripts/install.sh` is added and documented in the [README.md] to simplify the install procedure. --- README.md | 94 ++++++++++---- docs/cli/git_hooks.md | 2 +- docs/cli/git_hooks_install.md | 2 +- docs/cli/git_hooks_installer.md | 17 ++- githooks/cmd/installer/args.go | 5 +- githooks/cmd/installer/installer.go | 132 +++++++++++--------- githooks/cmd/update/update.go | 2 +- scripts/install.sh | 186 ++++++++++++++++++++++++++++ tests/steps/step-064.sh | 8 +- 9 files changed, 360 insertions(+), 88 deletions(-) create mode 100755 scripts/install.sh diff --git a/README.md b/README.md index 717e1aa3..04b760fa 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/nlohmann/json/master/LICENSE.MIT) [![GitHub Releases](https://img.shields.io/github/release/gabyx/githooks.svg)](https://github.com/gabyx/githooks/releases) ![Git Version](https://img.shields.io/badge/Git-%E2%89%A5v2.28.0,%20latest%20tests%20v2.36.1-blue) -![Go Version](https://img.shields.io/badge/Go-1.17-blue) +![Go Version](https://img.shields.io/badge/Go-1.20-blue) ![OS](https://img.shields.io/badge/OS-linux,%20macOs,%20Windows-blue) A **platform-independend hooks manager** written in Go to support shared hook @@ -132,6 +132,8 @@ Take this snippet of a Git repository layout as an example: │ │ ├── 01-validate # Normal hook script. │ │ └── 02-upload # Normal hook script. │ │ +│ ├── post-merge # An executable file. +│ │ │ ├── post-checkout/ # All post-checkout hooks. │ │ ├── .all-parallel # All hooks in this folder run in parallel. │ │ └── ... @@ -147,12 +149,12 @@ Take this snippet of a Git repository layout as an example: All hooks to be executed live under the `.githooks` top-level folder, that should be checked into the repository. Inside, we can have directories with the name of the hook (like `commit-msg` and `pre-commit` above), or a file matching -the hook name (like `post-checkout` in the example). The filenames in the -directory do not matter, but the ones starting with a `.` (dotfiles) will be -excluded by default. All others are executed in lexical order according to the -Go function [`Walk`](https://golang.org/pkg/path/filepath/#Walk). rules. -Subfolders as e.g. `final` get treated as parallel batch and all hooks inside -are by default executed in parallel over the thread pool. See +the hook name (like `post-merge` in the example). The filenames in the directory +do not matter, but the ones starting with a `.` (dotfiles) will be excluded by +default. All others are executed in lexical order according to the Go function +[`Walk`](https://golang.org/pkg/path/filepath/#Walk) rules. Subfolders as e.g. +`final` get treated as parallel batch and all hooks inside are by default +executed in parallel over the thread pool. See [Parallel Execution](#parallel-execution) for details. You can use the [command line helper](docs/cli/git_hooks.md) (a globally @@ -878,29 +880,62 @@ infastructure, docker container, etc...) and need to be disabled. Setting ## Installation -- [Download the latest release](https://github.com/gabyx/githooks/releases), - extract it and execute the installer command by the below instructions. +### Quick (Secure) + +Launch the below shell command. It will download the release from Github and +launch the installer. + +**Note:** All downloaded files are checksum & signature checked. + +```shell +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash +``` + +See the next sections on different install options. + +**Note:** Use `bash -s -- -h` above to show the help message of the bootstrap +script and `bash -s -- -- ` to pass arguments to the installer, e.g. +`bash -s -- -- -h` to show the help. + +### Procedure The installer will: +1. Download the current binaries if `--update` is not given. Optionally it can + use a deploy settings file to specify where to get the binaries from. + (default is this repository here.) + +1. Verify the checksums and signature of the downloaded binaries. + +1. Launch the current (or new if `--update` is given) installer which proceeds + with the next steps. + 1. Find out where the Git templates directory is. + 1. From the `$GIT_TEMPLATE_DIR` environment variable. 2. With the `git config --get init.templateDir` command. 3. Checking the default `/usr/share/git-core/templates` folder. 4. Search on the filesystem for matching directories. 5. Offer to set up a new one, and make it `init.templateDir`. -2. Write all chosen Githooks run-wrappers into the chosen directory: + +1. Write all chosen Githooks run-wrappers into the chosen directory: + - either `init.templateDir` or - `core.hooksPath` depending on the install mode `--use-core-hooks-path`. -3. Offer to enable automatic update checks. -4. Offer to find existing Git repositories on the filesystem (disable with + +1. Offer to enable automatic update checks. + +1. Offer to find existing Git repositories on the filesystem (disable with `--skip-install-into-existing`) + 1. Install run-wrappers into them (`.git/hooks`). 2. Offer to add an intro README in their `.githooks` folder. -5. Install/update run-wrappers into all registered repositories: Repositories + +1. Install/update run-wrappers into all registered repositories: Repositories using Githooks get registered in the install folders `registered.yaml` file on their first hook invocation. -6. Offer to set up shared hook repositories. + +1. Offer to set up shared hook repositories. ### Normal Installation @@ -912,7 +947,7 @@ Its advised to only install Githooks for a selection of the supported hooks by using `--maintained-hooks` as ```shell -cli installer \ +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \ --maintained-hooks "!all, pre-commit, pre-merge-commit, prepare-commit-msg, commit-msg, post-commit" \ --maintained-hooks "pre-rebase, post-checkout, post-merge, pre-push" ``` @@ -926,7 +961,8 @@ If you want, you can try out what the script would do first, without changing anything by using: ```shell -cli installer --dry-run +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \ + --dry-run ``` ### Install Mode: Centralized Hooks @@ -937,7 +973,8 @@ and the default one [below](#templates-or-central-hooks). For this, run the command below. ```shell -cli installer --use-core-hookspath +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \ + --use-core-hookspath ``` Optionally, you can also pass the template directory to which you want to @@ -945,7 +982,9 @@ install the centralized hooks by appending `--template-dir ` to the command above, for example: ```shell -cli installer --use-core-hookspath --template-dir /home/public/.githooks +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \ + --use-core-hookspath + --template-dir /home/public/.githooks ``` ### Install from different URL and Branch @@ -955,7 +994,9 @@ companies fork), you can specify the repository clone url as well as the branch name (default: `main`) when installing with: ```shell -cli installer --clone-url "https://server.com/my-githooks-fork.git" --clone-branch "release" +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \ + --clone-url "https://server.com/my-githooks-fork.git" \ + --clone-branch "release" ``` The installer always maintains a Githooks clone inside `/release` @@ -1013,7 +1054,8 @@ The global install prefix defaults to `${HOME}` but can be changed by using the options `--prefix `: ```shell -cli installer --non-interactive [--prefix ] +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \ + --non-interactive [--prefix ] ``` It's possible to specify which template directory should be used, by passing the @@ -1021,7 +1063,8 @@ It's possible to specify which template directory should be used, by passing the the templates to be installed. ```shell -cli installer --template-dir "/home/public/.githooks-templates" +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \ + --template-dir "/home/public/.githooks-templates" ``` By default the script will install the hooks into the `~/.githooks/templates/` @@ -1033,7 +1076,8 @@ On a server infrastructure where only _bare_ repositories are maintained, it is best to maintain only server hooks. This can be achieved by installing with: ```shell -cli installer ---maintained-hooks "server" +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- -- \ + --maintained-hooks "server" ``` The global template directory then **only** contains the following run-wrappers @@ -1194,6 +1238,12 @@ If you want to get rid of this hook manager, you can execute the uninstaller git hooks uninstaller ``` +or + +```shell +curl -sL https://raw.githubusercontent.com/gabyx/githooks/main/scripts/install.sh | bash -s -- --uninstall +``` + This will delete the run-wrappers installed in the template directory, optionally the installed hooks from the existing local repositories, and reinstates any previous hooks that were moved during the installation. diff --git a/docs/cli/git_hooks.md b/docs/cli/git_hooks.md index 0da32292..c4a2a307 100644 --- a/docs/cli/git_hooks.md +++ b/docs/cli/git_hooks.md @@ -23,7 +23,7 @@ git hooks * [git hooks ignore](git_hooks_ignore.md) - Ignores or activates hook in the current repository. * [git hooks images](git_hooks_images.md) - Manage container images. * [git hooks install](git_hooks_install.md) - Installs Githooks run-wrappers into the current repository. -* [git hooks installer](git_hooks_installer.md) - Githooks installer application +* [git hooks installer](git_hooks_installer.md) - Githooks installer application. * [git hooks list](git_hooks_list.md) - Lists the active hooks in the current repository. * [git hooks readme](git_hooks_readme.md) - Manages the Githooks README in the current repository. * [git hooks shared](git_hooks_shared.md) - Manages the shared hook repositories. diff --git a/docs/cli/git_hooks_install.md b/docs/cli/git_hooks_install.md index de6cbc3a..a6668cd0 100644 --- a/docs/cli/git_hooks_install.md +++ b/docs/cli/git_hooks_install.md @@ -22,7 +22,7 @@ git hooks install [flags] to all hook names if `all` or `server` is not given as first argument: - `all` : All hooks supported by Githooks. - `server` : Only server hooks supported by Githooks. - You can list them seperatly or comma-separated in one argument. + You can list them separately or comma-separated in one argument. --non-interactive Install non-interactively. ``` diff --git a/docs/cli/git_hooks_installer.md b/docs/cli/git_hooks_installer.md index 5f7511ee..45823975 100644 --- a/docs/cli/git_hooks_installer.md +++ b/docs/cli/git_hooks_installer.md @@ -1,10 +1,15 @@ ## git hooks installer -Githooks installer application +Githooks installer application. ### Synopsis -Githooks installer application +Githooks installer application. +It downloads the Githooks artifacts of the current version +from a deploy source and verifies its checksums and signature. +Then it calls the installer on the new version which +will then run the installation procedure for Githooks. + See further information at https://github.com/gabyx/githooks/blob/main/README.md ``` @@ -15,9 +20,11 @@ git hooks installer [flags] ``` --log string Log file path (only for installer). - --dry-run Dry run the installation showing whats being done. + --dry-run Dry run the installation showing what's being done. --non-interactive Run the installation non-interactively without showing prompts. + --update Install and update directly to the latest + possible tag on the clone branch. --skip-install-into-existing Skip installation into existing repositories defined by a search path. --prefix string Githooks installation prefix such that @@ -30,7 +37,7 @@ git hooks installer [flags] to all hook names if `all` or `server` is not given as first argument: - `all` : All hooks supported by Githooks. - `server` : Only server hooks supported by Githooks. - You can list them seperatly or comma-separated in one argument. + You can list them separately or comma-separated in one argument. --use-core-hookspath If the install mode `core.hooksPath` should be used. --clone-url string The clone url from which Githooks should clone and install/update itself. Githooks tries to @@ -47,7 +54,7 @@ git hooks installer [flags] --build-from-source If the binaries are built from source instead of downloaded from the deploy url. --build-tags strings Build tags for building from source (get extended with defaults). - You can list them seperatly or comma-separated in one argument. + You can list them separately or comma-separated in one argument. --use-pre-release When fetching the latest installer, also consider pre-release versions. -h, --help help for installer ``` diff --git a/githooks/cmd/installer/args.go b/githooks/cmd/installer/args.go index af2e6545..2b1eda68 100644 --- a/githooks/cmd/installer/args.go +++ b/githooks/cmd/installer/args.go @@ -1,6 +1,6 @@ package installer -// Arguments repesents all CLI arguments for the installer. +// Arguments represents all CLI arguments for the installer. type Arguments struct { Config string @@ -15,6 +15,9 @@ type Arguments struct { DryRun bool NonInteractive bool + Update bool // Directly update to the latest possible tag on the clone branch. + // Before `2.3.3` that was always true. + SkipInstallIntoExisting bool // Skip install into existing repositories. MaintainedHooks []string // Maintain hooks by Githooks. diff --git a/githooks/cmd/installer/installer.go b/githooks/cmd/installer/installer.go index 219f2744..969f4deb 100644 --- a/githooks/cmd/installer/installer.go +++ b/githooks/cmd/installer/installer.go @@ -32,9 +32,14 @@ func NewCmd(ctx *ccm.CmdContext) *cobra.Command { var cmd = &cobra.Command{ Use: "installer [flags]", - Short: "Githooks installer application", - Long: "Githooks installer application\n" + - "See further information at https://github.com/gabyx/githooks/blob/main/README.md", + Short: "Githooks installer application.", + Long: `Githooks installer application. +It downloads the Githooks artifacts of the current version +from a deploy source and verifies its checksums and signature. +Then it calls the installer on the new version which +will then run the installation procedure for Githooks. + +See further information at https://github.com/gabyx/githooks/blob/main/README.md`, PreRun: ccm.PanicIfAnyArgs(ctx.Log), RunE: func(cmd *cobra.Command, _ []string) error { return runInstall(cmd, ctx, vi) @@ -69,7 +74,7 @@ var MaintainedHooksDesc = "Any argument can be a hook name '', 'all' o "to all hook names if 'all' or 'server' is not given as first argument:\n" + " - 'all' : All hooks supported by Githooks.\n" + " - 'server' : Only server hooks supported by Githooks.\n" + - "You can list them seperatly or comma-separated in one argument." + "You can list them separately or comma-separated in one argument." func defineArguments(cmd *cobra.Command, vi *viper.Viper) { // Internal commands @@ -87,11 +92,15 @@ func defineArguments(cmd *cobra.Command, vi *viper.Viper) { // User commands cmd.PersistentFlags().Bool("dry-run", false, - "Dry run the installation showing whats being done.") + "Dry run the installation showing what's being done.") cmd.PersistentFlags().Bool( "non-interactive", false, "Run the installation non-interactively\n"+ "without showing prompts.") + cmd.PersistentFlags().Bool( + "update", false, + "Install and update directly to the latest\n"+ + "possible tag on the clone branch.") cmd.PersistentFlags().Bool( "skip-install-into-existing", false, "Skip installation into existing repositories\n"+ @@ -141,7 +150,7 @@ func defineArguments(cmd *cobra.Command, vi *viper.Viper) { cmd.PersistentFlags().StringSlice( "build-tags", nil, "Build tags for building from source (get extended with defaults).\n"+ - "You can list them seperatly or comma-separated in one argument.") + "You can list them separately or comma-separated in one argument.") cmd.PersistentFlags().Bool( "use-pre-release", false, @@ -157,6 +166,8 @@ func defineArguments(cmd *cobra.Command, vi *viper.Viper) { vi.BindPFlag("dryRun", cmd.PersistentFlags().Lookup("dry-run"))) cm.AssertNoErrorPanic( vi.BindPFlag("nonInteractive", cmd.PersistentFlags().Lookup("non-interactive"))) + cm.AssertNoErrorPanic( + vi.BindPFlag("update", cmd.PersistentFlags().Lookup("update"))) cm.AssertNoErrorPanic( vi.BindPFlag("skipInstallIntoExisting", cmd.PersistentFlags().Lookup("skip-install-into-existing"))) cm.AssertNoErrorPanic( @@ -273,7 +284,7 @@ func buildFromSource( branch string, commitSHA string) updates.Binaries { - log.Info("Building binaries from source ...") + log.Info("Building binaries from source at commit '%s'.", commitSHA) // Clone another copy of the release clone into temporary directory log.InfoF("Clone to temporary build directory '%s'", tempDir) @@ -395,8 +406,12 @@ func runInstallDispatched( "Could not get status of release clone '%s'", settings.CloneDir) + cm.PanicIfF(!status.IsUpdateAvailable, + "An autoupdate should only be triggered when and update is found.") + } else { log.Info("Fetching update in Githooks clone...") + status, err = updates.FetchUpdates( settings.CloneDir, args.CloneURL, @@ -413,62 +428,61 @@ func runInstallDispatched( log.DebugF("Status: %v", status) } - cm.PanicIfF(args.InternalAutoUpdate && !status.IsUpdateAvailable, - "An autoupdate should only be triggered when and update is found.") - installer := hooks.GetInstallerExecutable(settings.InstallDir) haveInstaller := cm.IsFile(installer.Cmd) - // We download/build the binaries if an update is available - // or the installer is missing. - binaries := updates.Binaries{} - log.InfoF("Githooks update available: '%v'", status.IsUpdateAvailable) log.InfoF("Githooks installer existing: '%v'", haveInstaller) - if status.IsUpdateAvailable || !haveInstaller { + // We download/build the binaries always. + doUpdate := status.IsUpdateAvailable && (args.Update || args.InternalAutoUpdate) + tag := "" + commit := "" - log.Info("Getting Githooks binaries ...") + if doUpdate { + tag = status.UpdateTag + commit = status.UpdateCommitSHA + } else { + tag = status.LocalTag + commit = status.LocalCommitSHA + } - tempDir, err := os.MkdirTemp(os.TempDir(), "githooks-update-*") - log.AssertNoErrorPanic(err, "Can not create temporary update dir in '%s'", os.TempDir()) - cleanUpX.AddHandler(func() { - _ = os.RemoveAll(tempDir) // @todo does not remove write protected files (go build) - }) - defer os.RemoveAll(tempDir) - - buildFromSrc := args.BuildFromSource || - gitx.GetConfig(hooks.GitCKBuildFromSource, git.GlobalScope) == git.GitCVTrue - - if buildFromSrc { - log.Info("Building from source...") - binaries = buildFromSource( - log, - cleanUpX, - args.BuildTags, - tempDir, - status.RemoteURL, - status.Branch, - status.RemoteCommitSHA) - } + binaries := updates.Binaries{} + log.InfoF("Getting Githooks binaries at version '%s' ...", tag) - // We need to run deploy code too when running coverage because - // it builds a non-instrumented binary. - if !buildFromSrc || IsRunningCoverage { - tag := status.UpdateTag - if strs.IsEmpty(tag) { - tag = status.LocalTag - } + tempDir, err := os.MkdirTemp(os.TempDir(), "githooks-update-*") + log.AssertNoErrorPanic(err, "Can not create temporary update dir in '%s'", os.TempDir()) + cleanUpX.AddHandler(func() { + _ = os.RemoveAll(tempDir) // @todo does not remove write protected files (go build) + }) + defer os.RemoveAll(tempDir) - log.InfoF("Download '%s' from deploy source...", tag) + buildFromSrc := args.BuildFromSource || + gitx.GetConfig(hooks.GitCKBuildFromSource, git.GlobalScope) == git.GitCVTrue - deploySettings := getDeploySettings(log, settings.InstallDir, status.RemoteURL, &args) - binaries = downloadBinaries(log, deploySettings, tempDir, tag) - } + if buildFromSrc { + log.Info("Building from source...") + binaries = buildFromSource( + log, + cleanUpX, + args.BuildTags, + tempDir, + status.RemoteURL, + status.Branch, + commit) + } + + // We need to run deploy code too when running coverage because + // it builds a non-instrumented binary. + if !buildFromSrc || IsRunningCoverage { + log.InfoF("Download '%s' from deploy source...", tag) - installer.Cmd = binaries.Cli + deploySettings := getDeploySettings(log, settings.InstallDir, status.RemoteURL, &args) + binaries = downloadBinaries(log, deploySettings, tempDir, tag) } + installer.Cmd = binaries.Cli + // Set variables for further update procedure... // Note: `args` is passed by value. args.InternalPostDispatch = true @@ -482,12 +496,13 @@ func runInstallDispatched( return false, nil } - log.PanicIfF(!cm.IsFile(installer.Cmd), "Githooks executable '%s' is not existing.", installer) + log.PanicIfF(!cm.IsFile(installer.Cmd), + "Githooks executable '%s' is not existing.", installer) - return true, runInstaller(log, &installer, &args) + return true, dispatchToInstaller(log, &installer, &args) } -func runInstaller(log cm.ILogContext, installer cm.IExecutable, args *Arguments) error { +func dispatchToInstaller(log cm.ILogContext, installer cm.IExecutable, args *Arguments) error { log.Info("Dispatching to new installer ...") @@ -1173,7 +1188,7 @@ func storeSettings(log cm.ILogContext, settings *Settings, uiSettings *install.U func updateClone(log cm.ILogContext, cloneDir string, updateToSHA string) { if strs.IsEmpty(updateToSHA) { - return // We don't need to update the relase clone. + return // We don't need to update the release clone. } commitSHA, err := updates.MergeUpdates(cloneDir, false) @@ -1198,14 +1213,18 @@ func thankYou(log cm.ILogContext) { "Thanks!\n", hooks.GithooksWebpage) } -func runUpdate( +func runInstaller( log cm.ILogContext, gitx *git.Context, settings *Settings, uiSettings *install.UISettings, args *Arguments) { - log.InfoF("Running install to version '%s' ...", build.BuildVersion) + if strs.IsEmpty(args.InternalUpdateFromVersion) { + log.InfoF("Running install to version '%s' ...", build.BuildVersion) + } else { + log.InfoF("Running install from '%s' -> '%s' ...", args.InternalUpdateFromVersion, build.BuildVersion) + } transformLegacyGitConfigSettings(log, gitx) @@ -1381,6 +1400,7 @@ func runInstall(cmd *cobra.Command, ctx *ccm.CmdContext, vi *viper.Viper) error if !args.InternalPostDispatch { assertOneInstallerRunning(log, ctx.CleanupX) + // Dispatch from an old installer to a new one. isDispatched, err := runInstallDispatched(log, ctx.GitX, &settings, args, ctx.CleanupX) log.MoveFileWriterToEnd() // We are logging to the same file. Move it to the end. if err != nil { @@ -1393,7 +1413,7 @@ func runInstall(cmd *cobra.Command, ctx *ccm.CmdContext, vi *viper.Viper) error // intended fallthrough ... (only debug) } - runUpdate(log, ctx.GitX, &settings, &uiSettings, &args) + runInstaller(log, ctx.GitX, &settings, &uiSettings, &args) if logStats.ErrorCount() == 0 { thankYou(log) diff --git a/githooks/cmd/update/update.go b/githooks/cmd/update/update.go index c5af037d..9fcb21c4 100644 --- a/githooks/cmd/update/update.go +++ b/githooks/cmd/update/update.go @@ -36,7 +36,7 @@ func runUpdate( func() error { installer := installer.NewCmd(ctx) - args := []string{} // should not be empty, because of SetArgs + args := []string{"--update"} if usePreRelease { args = append(args, "--use-pre-release") } diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 00000000..63f361d8 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash +set -euo pipefail + +function checkTool() { + if ! command -v "$1" &>/dev/null; then + echo "!! Required tool '$1' is not installed." + exit 1 + fi +} + +checkTool "jq" +checkTool "curl" +checkTool "sha256sum" +checkTool "tar" +checkTool "unzip" +checkTool "uname" + +org=gabyx +repo=githooks + +unInstall="false" +installerArgs=() +versionTag="" + +# Compare a and b as version strings. Rules: +# $1-a $2-op $3-b +# R1: a and b : dot-separated sequence of items. Items are numeric. The last item can optionally end with letters, i.e., 2.5 or 2.5a. +# R2: Zeros are automatically inserted to compare the same number of items, i.e., 1.0 < 1.0.1 means 1.0.0 < 1.0.1 => yes. +# R3: op can be '=' '==' '!=' '<' '<=' '>' '>=' (lexicographic). +# R4: Unrestricted number of digits of any item, i.e., 3.0003 > 3.0000004. +# R5: Unrestricted number of items. +function versionCompare() { + local a=$1 op=$2 b=$3 al=${1##*.} bl=${3##*.} + while [[ $al =~ ^[[:digit:]] ]]; do al=${al:1}; done + while [[ $bl =~ ^[[:digit:]] ]]; do bl=${bl:1}; done + local ai=${a%"$al"} bi=${b%"$bl"} + + local ap=${ai//[[:digit:]]/} bp=${bi//[[:digit:]]/} + ap=${ap//./.0} bp=${bp//./.0} + + local w=1 fmt=$a.$b x IFS=. + for x in $fmt; do [ ${#x} -gt "$w" ] && w=${#x}; done + fmt=${*//[^.]/} + fmt=${fmt//./%${w}s} + # shellcheck disable=SC2086,SC2059 + printf -v a "$fmt" $ai$bp + printf -v a "%s-%${w}s" "$a" "$al" + # shellcheck disable=SC2086,SC2059 + printf -v b "$fmt" $bi$ap + printf -v b "%s-%${w}s" "$b" "$bl" + + # shellcheck disable=SC1072 + case $op in + '<=' | '>=') test "$a" "${op:0:1}" "$b" || [ "$a" = "$b" ] ;; + *) test "$a" "$op" "$b" ;; + esac +} + +function printHelp() { + echo -e "Usage: install.sh [options...] [-- ...]\n\n" \ + "Options:\n" \ + " --version : The version to download (if not latest)\n" \ + " and install.\n" \ + " --uninstall : Uninstall Githooks. Uses always the latest uninstaller.\n" \ + "all other arguments are forwarded to the installer." +} + +function parseArgs() { + local toInstaller=false + local prev="" + + for p in "$@"; do + if [ "$toInstaller" = "true" ]; then + installerArgs+=("$p") + elif [ "$p" = "--help" ] || [ "$p" = "-h" ]; then + printHelp + exit 0 + elif [ "$p" = "--version" ]; then + true + elif [ "$p" = "--uninstall" ]; then + unInstall="true" + elif [ "$prev" = "--version" ]; then + versionTag="v$p" + + elif [ "$p" = "--" ]; then + toInstaller="true" + else + echo "! Unknown argument '$p'." >&2 + return 1 + fi + + prev="$p" + done +} + +parseArgs "$@" + +if [ "$versionTag" = "" ] || [ "$unInstall" = "true" ]; then + # Find the latest version using the GitHub API + response=$(curl --silent --location "https://api.github.com/repos/$org/$repo/releases") || { + echo "Could not get releases info from github.com" + exit 1 + } + + versionTag="$(echo "$response" | + jq --raw-output 'map(select((.assets | length) > 0)) | .[0].tag_name')" +fi + +if ! versionCompare "${versionTag##v}" ">=" "2.3.4"; then + echo "!! Can only bootstrap version tags >= 'v2.3.4' with this script. Got tag '$versionTag'." + exit 1 +fi + +systemName="$(uname | tr '[:upper:]' '[:lower:]')" +systemArch="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')" + +# Download and install +response=$(curl --silent --location "https://api.github.com/repos/$org/$repo/releases/tags/$versionTag") || { + echo "Could not get releases from github.com." + exit 1 +} + +checksumFileURL=$(echo "$response" | jq --raw-output ".assets[] | select( .name == \"githooks.checksums\") | .browser_download_url") + +url=$(echo "$response" | + jq --raw-output ".assets[] | select( (.name | contains(\"$systemName\")) and (.name | contains(\"$systemArch\")) ) | .browser_download_url") || { + echo "Could not get assets from tag '$versionTag'." + exit 1 +} + +if [ -z "$url" ]; then + echo -e "!! Unsupported operating system '$systemName' or \n" \ + "machine type '$systemArch': \n" \ + "Please check 'https://github.com/$org/${repo}/releases' manually." + + exit 1 +fi + +tempDir="$(mktemp -d)" + +function cleanUp() { + rm -rf "$tempDir" &>/dev/null || true +} +trap cleanUp EXIT + +githooks="$tempDir/githooks" +mkdir -p "$githooks" + +cliExe="cli" +if [ "$systemName" = "windows" ]; then + cliExe="$cliExe.exe" +fi + +cd "$tempDir" + +echo -e "Downloading '$checksumFileURL'..." +checksums=$(curl --progress-bar --location "$checksumFileURL") + +echo -e "Downloading '$url'..." +curl --progress-bar --location "$url" -o githooks.install + +checksum=$(sha256sum "githooks.install" | cut -d ' ' -f 1) +if ! echo "$checksums" | grep -q "$checksum"; then + echo "!! Checksum sha265 '$checksum' could not be verified in 'githooks.checksums' file." + echo "$checksums" + + exit 1 +else + echo -e "\n=============================\nChecksums verified!\n=============================\n" +fi + +case "$url" in +*.tar.gz) + tar -C "$githooks" -xzf "githooks.install" >/dev/null + ;; + +*.zip) + unzip -d "$githooks" "$githooks.install" >/dev/null + ;; +esac + +if [ "$unInstall" = "true" ]; then + "githooks/$cliExe" uninstaller "${installerArgs[@]}" +else + "githooks/$cliExe" installer "${installerArgs[@]}" +fi diff --git a/tests/steps/step-064.sh b/tests/steps/step-064.sh index 9d147dfe..06b4515d 100755 --- a/tests/steps/step-064.sh +++ b/tests/steps/step-064.sh @@ -24,9 +24,15 @@ fi # Set to build from source git config --global githooks.buildFromSource "true" +if ! "$GH_INSTALL_BIN_DIR/cli" --version | grep -q "9.9.0"; then + echo "! Expected to update to 9.9.0" + "$GH_INSTALL_BIN_DIR/cli" --version + exit 1 +fi + CURRENT="$(git -C ~/.githooks/release rev-parse HEAD)" if ! OUT=$("$GH_INSTALL_BIN_DIR/cli" update --yes); then - echo "! Failed to run the update" + echo -e "! Failed to run the update:\n$OUT" fi if ! echo "$OUT" | grep -qi "building from source"; then