diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1e389936..2f30c5af 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,11 +10,11 @@ jobs: name: golangci-lint runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.22.x - - uses: actions/checkout@v4 + go-version-file: go.mod - name: golangci-lint uses: golangci/golangci-lint-action@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c4623610..ec7c4b1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: goreleaser +name: release on: push: @@ -23,10 +23,10 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.22.x + go-version-file: go.mod - name: Install Snapcraft - uses: samuelmeuli/action-snapcraft@v1 + uses: samuelmeuli/action-snapcraft@v2 - name: Prevent from snapcraft fail run: | @@ -34,11 +34,11 @@ jobs: mkdir -p $HOME/.cache/snapcraft/stage-packages - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser - version: latest - args: release --clean + version: '~> v2' + args: release --clean --verbose env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} @@ -59,8 +59,8 @@ jobs: EOF chmod 0600 ~/.gem/credentials cd packaging/ - make prepare - make publish + ruby pack.rb prepare + ruby pack.rb publish - name: Update Homebrew formula uses: dawidd6/action-homebrew-bump-formula@v3 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e63f537..110bbef6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,16 +9,15 @@ jobs: test: strategy: matrix: - go-version: [1.22.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: - go-version: ${{ matrix.go-version }} - - name: Checkout code - uses: actions/checkout@v4 + go-version-file: go.mod - name: Test run: go test ./... -coverprofile coverage.out - name: Report coverage @@ -31,24 +30,27 @@ jobs: test-integrity: strategy: matrix: - go-version: [1.22.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} env: GOCOVERDIR: ${{ github.workspace }}/_icoverdir_ steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: - go-version: ${{ matrix.go-version }} - - name: Checkout code - uses: actions/checkout@v4 + go-version-file: go.mod - name: Prepare lefthook run: | mkdir _icoverdir_ go install -cover - name: Run integrity tests - run: go test integrity_test.go -tags=integrity + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + command: go test integrity_test.go -tags=integrity - name: Collect coverage run: | go tool covdata textfmt -i _icoverdir_ -o coverage.out @@ -63,18 +65,18 @@ jobs: build: runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v4 - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.22.x - - name: Checkout code - uses: actions/checkout@v4 + go-version-file: go.mod - name: Build binaries - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser - version: latest - args: build --snapshot --skip-validate --clean + version: '~> v2' + args: release --snapshot --skip=publish --skip=snapcraft --skip=validate --clean --verbose - name: Tar binaries to preserve executable bit run: 'tar -cvf lefthook-binaries.tar --directory dist/ $(find dist/ -executable -type f -printf "%P\0" | xargs --null)' - name: Upload binaries as artifacts @@ -91,5 +93,5 @@ jobs: steps: - uses: coverallsapp/github-action@v2 with: - carryforward: "integration-1.22.x ubuntu-latest,integration-1.22.x macos-latest,integration-1.22.x windows-latest,1.22.x ubuntu-latest,1.22.x macos-latest,1.22.x windows-latest" + carryforward: "integration-ubuntu-latest,integration-macos-latest,integration-windows-latest,ubuntu-latest,macos-latest,windows-latest" parallel-finished: true diff --git a/.gitignore b/.gitignore index 3fb90110..9d02edbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,17 @@ .vscode/ .idea/ -/lefthook-local.yml /lefthook +/lefthook-local.yml tmp/ dist/ + +# Packages packaging/rubygems/pkg/ packaging/rubygems/libexec/ packaging/npm-bundled/bin/ packaging/npm-*/README.md packaging/npm/*/bin/ -!packaging/npm/lefthook/bin/index.js packaging/npm/*/README.md -package.json !packaging/npm/*/package.json -node_modules/ -yarn.lock -package-lock.json +!packaging/npm/lefthook/bin/index.js diff --git a/.goreleaser.yml b/.goreleaser.yml index 7136f89e..743254c1 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,31 +1,66 @@ +version: 2 project_name: lefthook before: hooks: - go generate ./... builds: -- env: - - CGO_ENABLED=0 - goos: - - linux - - darwin - - windows - - freebsd - goarch: - - amd64 - - arm64 - - 386 - ignore: - - goos: darwin - goarch: 386 - - goos: linux - goarch: 386 - - goos: freebsd - goarch: 386 - ldflags: - - -s -w -X github.com/evilmartians/lefthook/internal/version.commit={{.Commit}} + # Builds the binaries without `lefthook upgrade` + - id: no_self_update + tags: + - no_self_update + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + - freebsd + goarch: + - amd64 + - arm64 + - 386 + ignore: + - goos: darwin + goarch: 386 + - goos: linux + goarch: 386 + - goos: freebsd + goarch: 386 + flags: + - -trimpath + ldflags: + - -s -w -X github.com/evilmartians/lefthook/internal/version.commit={{.Commit}} + + # Full lefthook binary + - id: lefthook + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + - freebsd + goarch: + - amd64 + - arm64 + - 386 + ignore: + - goos: darwin + goarch: 386 + - goos: linux + goarch: 386 + - goos: freebsd + goarch: 386 + flags: + - -trimpath + ldflags: + - -s -w -X github.com/evilmartians/lefthook/internal/version.commit={{.Commit}} + archives: - id: lefthook format: binary + builds: + - lefthook files: - none* name_template: >- @@ -36,8 +71,11 @@ archives: {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} + - id: lefthook-gz format: gz + builds: + - lefthook files: - none* name_template: >- @@ -48,10 +86,13 @@ archives: {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} + checksum: name_template: '{{ .ProjectName }}_checksums.txt' + snapshot: name_template: "{{ .Tag }}" + changelog: sort: asc filters: @@ -64,25 +105,27 @@ changelog: - '^\d+\.\d+\.\d+:' snapcrafts: - - - summary: Fast and powerful Git hooks manager for any type of projects. + - summary: Fast and powerful Git hooks manager for any type of projects. description: | Lefthook is a single dependency-free binary to manage all your git hooks. It works with any language in any environment, and in all common team workflows. grade: stable confinement: classic publish: true license: MIT + builds: + - no_self_update nfpms: - - - file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' + - file_name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' homepage: https://github.com/evilmartians/lefthook description: Lefthook a single dependency-free binary to manage all your git hooks that works with any language in any environment, and in all common team workflows - maintainer: Alexander Abroskin + maintainer: Evil Martians license: MIT vendor: Evil Martians + builds: + - no_self_update formats: - - deb - - rpm + - deb + - rpm dependencies: - - git + - git diff --git a/CHANGELOG.md b/CHANGELOG.md index 33079f01..164d2ac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,78 @@ ## master (unreleased) +## 1.7.9 (2024-07-26) + +- fix: typo CGO_ENABLED instead of GCO_ENABLED ([#791](https://github.com/evilmartians/lefthook/pull/791)) by @mrexox + +## 1.7.8 (2024-07-26) + +- fix: npm fix packages ([#789](https://github.com/evilmartians/lefthook/pull/789)) by @mrexox +- fix: explicitly pass static flag to linker ([#788](https://github.com/evilmartians/lefthook/pull/788)) by @mrexox +- ci: update workflow files ([#787](https://github.com/evilmartians/lefthook/pull/787)) by @mrexox +- ci: use latest goreleaser ([#784](https://github.com/evilmartians/lefthook/pull/784)) by @mrexox + +## 1.7.7 (2024-07-24) + +- fix: multiple excludes ([#782](https://github.com/evilmartians/lefthook/pull/782)) by @mrexox + +## 1.7.6 (2024-07-24) + +- feat: add self-update command ([#778](https://github.com/evilmartians/lefthook/pull/778)) by @mrexox + +## 1.7.5 (2024-07-22) + +- feat: use glob in exclude array ([#777](https://github.com/evilmartians/lefthook/pull/777)) by @mrexox + +## 1.7.4 (2024-07-19) + +- fix: rollback packaging changes ([#776](https://github.com/evilmartians/lefthook/pull/776)) by @mrexox + +## 1.7.3 (2024-07-18) + +- feat: allow list of files in exclude option ([#772](https://github.com/evilmartians/lefthook/pull/772)) by @mrexox +- docs: add docs for LEFTHOOK_OUTPUT var ([#771](https://github.com/evilmartians/lefthook/pull/771)) by @manbearwiz +- fix: use direct lefthook package ([#774](https://github.com/evilmartians/lefthook/pull/774)) by @mrexox + +## 1.7.2 (2024-07-11) + +- fix: add missing sub directory in hook template ([#768](https://github.com/evilmartians/lefthook/pull/768)) by @nikeee + +## 1.7.1 (2024-07-08) + +- fix: use correct extension in hook.tmpl ([#767](https://github.com/evilmartians/lefthook/pull/767)) by @apfohl + +## 1.7.0 (2024-07-08) + +- fix: publishing ([#765](https://github.com/evilmartians/lefthook/pull/765)) by @mrexox +- perf: startup time reduce ([#705](https://github.com/evilmartians/lefthook/pull/705)) by @dalisoft +- docs: add a note about pnpm package installation ([#761](https://github.com/evilmartians/lefthook/pull/761)) by @mrexox +- ci: retriable integrity tests ([#758](https://github.com/evilmartians/lefthook/pull/758)) by @mrexox +- ci: universal publisher with Ruby script ([#756](https://github.com/evilmartians/lefthook/pull/756)) by @mrexox + +## 1.6.18 (2024-06-21) + +- fix: allow multiple levels of extends ([#755](https://github.com/evilmartians/lefthook/pull/755)) by @mrexox + +## 1.6.17 (2024-06-20) + +- fix: apply local extends only if they are present ([#754](https://github.com/evilmartians/lefthook/pull/754)) by @mrexox +- chore: setting proper error message for missing lefthook file ([#748](https://github.com/evilmartians/lefthook/pull/748)) by @Cadienvan + +## 1.6.16 (2024-06-13) + +- fix: skip overwriting hooks when fetching data from remotes ([#745](https://github.com/evilmartians/lefthook/pull/745)) by @mrexox +- fix: fetch remotes only for non ghost hooks ([#744](https://github.com/evilmartians/lefthook/pull/744)) by @mrexox + +## 1.6.15 (2024-06-03) + +- feat: add refetch option to remotes config ([#739](https://github.com/evilmartians/lefthook/pull/739)) by @mrexox +- deps: June, 3, lipgloss (0.11.0) and viper (1.19.0) ([#742](https://github.com/evilmartians/lefthook/pull/742)) by @mrexox +- chore: enable copyloopvar, intrange, and prealloc ([#740](https://github.com/evilmartians/lefthook/pull/740)) by @scop +- perf: delay git and uname commands in hook scripts until needed ([#737](https://github.com/evilmartians/lefthook/pull/737)) by @scop +- chore: refactor commands interfaces ([#735](https://github.com/evilmartians/lefthook/pull/735)) by @mrexox +- chore: upgrade to 1.59.0 ([#738](https://github.com/evilmartians/lefthook/pull/738)) by @scop + ## 1.6.14 (2024-05-30) - fix: share STDIN across different commands on pre-push hook ([#732](https://github.com/evilmartians/lefthook/pull/732)) by @tdesveaux and @mrexox diff --git a/Makefile b/Makefile index 3de1cbfc..e73c1409 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ lint: bin/golangci-lint version: @read -p "New version: " version sed -i "s/const version = .*/const version = \"$$version\"/" internal/version/version.go - sed -i "s/VERSION := .*/VERSION := $$version/" packaging/Makefile + sed -i "s/VERSION = .*/VERSION = \"$$version\"/" packaging/pack.rb sed -i "s/lefthook-plugin.git\", exact: \".*\"/lefthook-plugin.git\", exact: \"$$version\"/" docs/install.md - make -C packaging clean set-version + ruby packaging/pack.rb clean set_version git add internal/version/version.go packaging/* docs/install.md diff --git a/cmd/add.go b/cmd/add.go index ad3f1f16..871727ba 100644 --- a/cmd/add.go +++ b/cmd/add.go @@ -12,7 +12,9 @@ import ( //go:embed add-doc.txt var addDoc string -func newAddCmd(opts *lefthook.Options) *cobra.Command { +type add struct{} + +func (add) New(opts *lefthook.Options) *cobra.Command { args := lefthook.AddArgs{} addHookCompletions := func(cmd *cobra.Command, args []string, toComplete string) (ret []string, compDir cobra.ShellCompDirective) { diff --git a/cmd/commands.go b/cmd/commands.go new file mode 100644 index 00000000..4871458f --- /dev/null +++ b/cmd/commands.go @@ -0,0 +1,23 @@ +//go:build !no_self_update + +package cmd + +import ( + "github.com/spf13/cobra" + + "github.com/evilmartians/lefthook/internal/lefthook" +) + +type command interface { + New(*lefthook.Options) *cobra.Command +} + +var commands = [...]command{ + version{}, + add{}, + install{}, + uninstall{}, + run{}, + dump{}, + selfUpdate{}, +} diff --git a/cmd/commands_no_self_update.go b/cmd/commands_no_self_update.go new file mode 100644 index 00000000..56daa0d5 --- /dev/null +++ b/cmd/commands_no_self_update.go @@ -0,0 +1,22 @@ +//go:build no_self_update + +package cmd + +import ( + "github.com/spf13/cobra" + + "github.com/evilmartians/lefthook/internal/lefthook" +) + +type command interface { + New(*lefthook.Options) *cobra.Command +} + +var commands = [...]command{ + version{}, + add{}, + install{}, + uninstall{}, + run{}, + dump{}, +} diff --git a/cmd/dump.go b/cmd/dump.go index e0071fc3..13d38744 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -6,7 +6,9 @@ import ( "github.com/evilmartians/lefthook/internal/lefthook" ) -func newDumpCmd(opts *lefthook.Options) *cobra.Command { +type dump struct{} + +func (dump) New(opts *lefthook.Options) *cobra.Command { dumpArgs := lefthook.DumpArgs{} dumpCmd := cobra.Command{ Use: "dump", diff --git a/cmd/install.go b/cmd/install.go index f27dbf5e..6d020212 100644 --- a/cmd/install.go +++ b/cmd/install.go @@ -7,7 +7,9 @@ import ( "github.com/evilmartians/lefthook/internal/log" ) -func newInstallCmd(opts *lefthook.Options) *cobra.Command { +type install struct{} + +func (install) New(opts *lefthook.Options) *cobra.Command { var a, force bool installCmd := cobra.Command{ diff --git a/cmd/root.go b/cmd/root.go index c3ecae63..2190990a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -9,15 +9,6 @@ import ( "github.com/evilmartians/lefthook/internal/log" ) -var commands = [...]func(*lefthook.Options) *cobra.Command{ - newVersionCmd, - newAddCmd, - newInstallCmd, - newUninstallCmd, - newRunCmd, - newDumpCmd, -} - func newRootCmd() *cobra.Command { options := lefthook.Options{ Fs: afero.NewOsFs(), @@ -61,7 +52,7 @@ func newRootCmd() *cobra.Command { } for _, subcommand := range commands { - rootCmd.AddCommand(subcommand(&options)) + rootCmd.AddCommand(subcommand.New(&options)) } return rootCmd diff --git a/cmd/run.go b/cmd/run.go index a07d2985..c7b48615 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -8,7 +8,9 @@ import ( "github.com/evilmartians/lefthook/internal/log" ) -func newRunCmd(opts *lefthook.Options) *cobra.Command { +type run struct{} + +func (run) New(opts *lefthook.Options) *cobra.Command { runArgs := lefthook.RunArgs{} runHookCompletions := func(cmd *cobra.Command, args []string, toComplete string) (ret []string, compDir cobra.ShellCompDirective) { diff --git a/cmd/self_update.go b/cmd/self_update.go new file mode 100644 index 00000000..21e48e81 --- /dev/null +++ b/cmd/self_update.go @@ -0,0 +1,73 @@ +package cmd + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/spf13/cobra" + + "github.com/evilmartians/lefthook/internal/lefthook" + "github.com/evilmartians/lefthook/internal/log" + "github.com/evilmartians/lefthook/internal/updater" +) + +type selfUpdate struct{} + +func (selfUpdate) New(opts *lefthook.Options) *cobra.Command { + var yes bool + upgradeCmd := cobra.Command{ + Use: "self-update", + Short: "Update lefthook executable", + Example: "lefthook self-update", + ValidArgsFunction: cobra.NoFileCompletions, + Args: cobra.NoArgs, + RunE: func(_cmd *cobra.Command, _args []string) error { + return update(opts, yes) + }, + } + + upgradeCmd.Flags().BoolVarP(&yes, "yes", "y", false, "no prompt") + upgradeCmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "force upgrade") + upgradeCmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "show verbose logs") + + return &upgradeCmd +} + +func update(opts *lefthook.Options, yes bool) error { + if os.Getenv(lefthook.EnvVerbose) == "1" || os.Getenv(lefthook.EnvVerbose) == "true" { + opts.Verbose = true + } + if opts.Verbose { + log.SetLevel(log.DebugLevel) + log.Debug("Verbose mode enabled") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Handle interrupts + signalChan := make(chan os.Signal, 1) + signal.Notify( + signalChan, + syscall.SIGINT, + syscall.SIGTERM, + ) + go func() { + <-signalChan + cancel() + }() + + exePath, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to determine the binary path: %w", err) + } + + return updater.New().SelfUpdate(ctx, updater.Options{ + Yes: yes, + Force: opts.Force, + ExePath: exePath, + }) +} diff --git a/cmd/uninstall.go b/cmd/uninstall.go index 7b3cdee7..470f85c5 100644 --- a/cmd/uninstall.go +++ b/cmd/uninstall.go @@ -6,7 +6,9 @@ import ( "github.com/evilmartians/lefthook/internal/lefthook" ) -func newUninstallCmd(opts *lefthook.Options) *cobra.Command { +type uninstall struct{} + +func (uninstall) New(opts *lefthook.Options) *cobra.Command { args := lefthook.UninstallArgs{} uninstallCmd := cobra.Command{ diff --git a/cmd/version.go b/cmd/version.go index b6c1a8e7..162ae0fa 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -5,10 +5,12 @@ import ( "github.com/evilmartians/lefthook/internal/lefthook" "github.com/evilmartians/lefthook/internal/log" - "github.com/evilmartians/lefthook/internal/version" + ver "github.com/evilmartians/lefthook/internal/version" ) -func newVersionCmd(_opts *lefthook.Options) *cobra.Command { +type version struct{} + +func (version) New(_opts *lefthook.Options) *cobra.Command { var verbose bool versionCmd := cobra.Command{ @@ -17,7 +19,7 @@ func newVersionCmd(_opts *lefthook.Options) *cobra.Command { ValidArgsFunction: cobra.NoFileCompletions, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - log.Println(version.Version(verbose)) + log.Println(ver.Version(verbose)) }, } diff --git a/docs/configuration.md b/docs/configuration.md index bdf21ef2..11270cd2 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -561,7 +561,7 @@ pre-commit: **Default: `false`** -> **Note** +> [!NOTE] > > Lefthook runs commands and scripts **sequentially** by default. @@ -571,7 +571,7 @@ Run commands and scripts concurrently. **Default: `false`** -> **Note** +> [!NOTE] > > Lefthook will return an error if both `piped: true` and `parallel: true` are set. @@ -613,7 +613,7 @@ pre-push: run: yarn test ``` -> **Note** +> [!NOTE] > > If used with [`parallel`](#parallel) the output can be a mess, so please avoid setting both options to `true`. @@ -786,7 +786,7 @@ pre-push: Simply run `bundle exec rubocop` on all files with `.rb` extension excluding `application.rb` and `routes.rb` files. -> **Note** +> [!NOTE] > > `--force-exclusion` will apply `Exclude` configuration setting of Rubocop. @@ -798,7 +798,9 @@ pre-commit: rubocop: tags: backend style glob: "*.rb" - exclude: '(^|/)(application|routes)\.rb$' + exclude: + - config/application.rb + - config/routes.rb run: bundle exec rubocop --force-exclusion {all_files} ``` @@ -1259,15 +1261,38 @@ pre-commit: ### `exclude` -You can provide a regular expression to exclude some files from being passed to [`run`](#run) command. +For the `exclude` option two variants are supported: + +- A list of globs to be excluded +- A single regular expression (deprecated) -The regular expression is matched against full paths to files in the repo, -relative to the repo root, using `/` as the directory separator on all platforms. -File paths do not begin with the separator or any other prefix. + +> [!NOTE] +> +> The regular expression is matched against full paths to files in the repo, +> relative to the repo root, using `/` as the directory separator on all platforms. +> File paths do not begin with the separator or any other prefix. **Example** -Run Rubocop on staged files with `.rb` extension except for `application.rb`, `routes.rb`, and `rails_helper.rb` (wherever they are). +Run Rubocop on staged files with `.rb` extension except for `application.rb`, `routes.rb`, `rails_helper.rb`, and all Ruby files in `config/initializers/`. + +```yml +# lefthook.yml + +pre-commit: + commands: + lint: + glob: "*.rb" + exclude: + - config/routes.rb + - config/application.rb + - config/initializers/*.rb + - spec/rails_helper.rb + run: bundle exec rubocop --force-exclusion {staged_files} +``` + +The same example using a regular expression. ```yml # lefthook.yml @@ -1276,7 +1301,7 @@ pre-commit: commands: lint: glob: "*.rb" - exclude: '(^|/)(application|routes|rails_helper)\.rb$' + exclude: '(^|/)(application|routes|rails_helper|initializers/\w+)\.rb$' run: bundle exec rubocop --force-exclusion {staged_files} ``` @@ -1348,7 +1373,7 @@ pre-commit: **Default: `false`** -> **Note** +> [!NOTE] > > If you want to pass stdin to your command or script but don't need to get the input from CLI, use [`use_stdin`](#use_stdin) option instead. @@ -1362,7 +1387,7 @@ Whether to use interactive mode. This applies the certain behavior: **Default: `0`** -> **Note** +> [!NOTE] > > This option makes sense only when `parallel: false` or `piped: true` is set. > @@ -1445,7 +1470,7 @@ When you try to commit `git commit -m "bad commit text"` script `template_checke ### `use_stdin` -> **Note** +> [!NOTE] > > With many commands or scripts having `use_stdin: true`, only one will receive the data. The others will have nothing. If you need to pass the data from stdin to every command or script, please, submit a [feature request](https://github.com/evilmartians/lefthook/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.md). diff --git a/docs/install.md b/docs/install.md index 0970c902..11f5c65d 100644 --- a/docs/install.md +++ b/docs/install.md @@ -57,6 +57,9 @@ Lefthook is available on NPM in the following flavors: yarn add -D @evilmartians/lefthook-installer ``` +> [!NOTE] +> If you use `pnpm` package manager make sure you set `side-effects-cache = false` in your .npmrc, otherwise the postinstall script of the lefthook package won't be executed and hooks won't be installed. + ## Go ```bash @@ -73,12 +76,12 @@ python3 -m pip install --user lefthook ## Swift -You can find the Swift wrapper plugin [here](https://github.com/csjones/lefthook-plugin). +You can find the Swift wrapper plugin [here](https://github.com/csjones/lefthook-plugin). Utilize lefthook in your Swift project using Swift Package Manager: ```swift -.package(url: "https://github.com/csjones/lefthook-plugin.git", exact: "1.6.14"), +.package(url: "https://github.com/csjones/lefthook-plugin.git", exact: "1.7.9"), ``` Or, with [mint](https://github.com/yonaskolb/Mint): diff --git a/docs/usage.md b/docs/usage.md index dec1bdf8..4524ceb5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -16,6 +16,7 @@ Then use git as usually, you don't need to reinstall lefthook when you change th - [Control behavior with ENV variables](#control-behavior-with-env-variables) - [`LEFTHOOK`](#lefthook) - [`LEFTHOOK_EXCLUDE`](#lefthook_exclude) + - [`LEFTHOOK_OUTPUT`](#lefthook_output) - [`LEFTHOOK_QUIET`](#lefthook_quiet) - [`LEFTHOOK_VERBOSE`](#lefthook_verbose) - [`LEFTHOOK_BIN`](#lefthook_bin) @@ -166,7 +167,7 @@ LEFTHOOK=0 git commit -am "Lefthook skipped" ### `LEFTHOOK_EXCLUDE` -Use `LEFTHOOK_EXCLUDE=`{list of tags or command names to be excluded} to skip some commands or scripts by tag or name (for commands only). See [`exclude_tags`](./configuration.md#exclude_tags) config option for more details. +Use `LEFTHOOK_EXCLUDE={list of tags or command names to be excluded}` to skip some commands or scripts by tag or name (for commands only). See the [`exclude_tags`](./configuration.md#exclude_tags) configuration option for more details. **Example** @@ -174,9 +175,21 @@ Use `LEFTHOOK_EXCLUDE=`{list of tags or command names to be excluded} to skip so LEFTHOOK_EXCLUDE=ruby,security,lint git commit -am "Skip some tag checks" ``` +### `LEFTHOOK_OUTPUT` + +Use `LEFTHOOK_OUTPUT={list of output values}` to specify what to print in your output. You can also set `LEFTHOOK_OUTPUT=false` to disable all output except for errors. Refer to the [`output`](./configuration.md#output) configuration option for more details. + +**Example** + +```bash +$ LEFTHOOK_OUTPUT=summary lefthook run pre-commit +summary: (done in 0.52 seconds) +✔️ lint +``` + ### `LEFTHOOK_QUIET` -You can skip some output printed by lefthook with `LEFTHOOK_QUIET` ENV variable. Just provide a list of output types. See [`skip_output`](./configuration.md#skip_output) config option for more details. +You can skip some outputs printed by lefthook by setting the `LEFTHOOK_QUIET` environment variable. Provide a list of output types to be skipped. See the [`skip_output`](./configuration.md#skip_output) configuration option for more details. **Example** diff --git a/go.mod b/go.mod index 1d14d152..4d7edc18 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,11 @@ require ( github.com/mattn/go-tty v0.0.7 github.com/mitchellh/mapstructure v1.5.0 github.com/rogpeppe/go-internal v1.12.0 + github.com/schollz/progressbar/v3 v3.14.4 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 ) @@ -50,7 +52,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.14.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index d80f244e..432a6b0c 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,7 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -67,6 +68,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74= +github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -99,10 +102,12 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjs golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= diff --git a/internal/config/command.go b/internal/config/command.go index 1e243da2..ad2db62e 100644 --- a/internal/config/command.go +++ b/internal/config/command.go @@ -23,9 +23,9 @@ type Command struct { FileTypes []string `json:"file_types,omitempty" mapstructure:"file_types" toml:"file_types,omitempty" yaml:"file_types,omitempty"` - Glob string `json:"glob,omitempty" mapstructure:"glob" toml:"glob,omitempty" yaml:",omitempty"` - Root string `json:"root,omitempty" mapstructure:"root" toml:"root,omitempty" yaml:",omitempty"` - Exclude string `json:"exclude,omitempty" mapstructure:"exclude" toml:"exclude,omitempty" yaml:",omitempty"` + Glob string `json:"glob,omitempty" mapstructure:"glob" toml:"glob,omitempty" yaml:",omitempty"` + Root string `json:"root,omitempty" mapstructure:"root" toml:"root,omitempty" yaml:",omitempty"` + Exclude interface{} `json:"exclude,omitempty" mapstructure:"exclude" toml:"exclude,omitempty" yaml:",omitempty"` Priority int `json:"priority,omitempty" mapstructure:"priority" toml:"priority,omitempty" yaml:",omitempty"` FailText string `json:"fail_text,omitempty" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"` diff --git a/internal/config/load.go b/internal/config/load.go index 7c1f060d..aa03aa43 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -5,6 +5,7 @@ import ( "fmt" "path/filepath" "regexp" + "slices" "strings" "github.com/spf13/afero" @@ -20,7 +21,10 @@ const ( DefaultSourceDirLocal = ".lefthook-local" ) -var hookKeyRegexp = regexp.MustCompile(`^(?P[^.]+)\.(scripts|commands)`) +var ( + hookKeyRegexp = regexp.MustCompile(`^(?P[^.]+)\.(scripts|commands)`) + localConfigNames = []string{"lefthook-local", ".lefthook-local"} +) // NotFoundError wraps viper.ConfigFileNotFoundError for lefthook. type NotFoundError struct { @@ -59,21 +63,27 @@ func Load(fs afero.Fs, repo *git.Repository) (*Config, error) { } func read(fs afero.Fs, path string, name string) (*viper.Viper, error) { + v := newViper(fs, path) + v.SetConfigName(name) + + if err := v.ReadInConfig(); err != nil { + return nil, err + } + + return v, nil +} + +func newViper(fs afero.Fs, path string) *viper.Viper { v := viper.New() v.SetFs(fs) v.AddConfigPath(path) - v.SetConfigName(name) // Allow overwriting settings with ENV variables v.SetEnvPrefix("LEFTHOOK") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() - if err := v.ReadInConfig(); err != nil { - return nil, err - } - - return v, nil + return v } func readOne(fs afero.Fs, path string, names []string) (*viper.Viper, error) { @@ -91,7 +101,7 @@ func readOne(fs afero.Fs, path string, names []string) (*viper.Viper, error) { return v, nil } - return nil, NotFoundError{fmt.Sprintf("No config files with names %q could not be found in \"%s\"", names, path)} + return nil, NotFoundError{fmt.Sprintf("No config files with names %q have been found in \"%s\"", names, path)} } // mergeAll merges configs using the following order. @@ -105,17 +115,25 @@ func mergeAll(fs afero.Fs, repo *git.Repository) (*viper.Viper, error) { return nil, err } - if err := extend(extends, repo.RootPath); err != nil { + if err := extend(fs, extends, repo.RootPath); err != nil { return nil, err } + // Save global extends to compare them after merging local config + globalExtends := extends.GetStringSlice("extends") + if err := mergeRemotes(fs, repo, extends); err != nil { return nil, err } - if err := mergeOne([]string{"lefthook-local", ".lefthook-local"}, "", extends); err == nil { - if err = extend(extends, repo.RootPath); err != nil { - return nil, err + //nolint:nestif + if err := mergeLocal(extends); err == nil { + // Local extends need to be re-applied only if they have different settings + localExtends := extends.GetStringSlice("extends") + if !slices.Equal(globalExtends, localExtends) { + if err = extend(fs, extends, repo.RootPath); err != nil { + return nil, err + } } } else { var notFoundErr viper.ConfigFileNotFoundError @@ -178,7 +196,7 @@ func mergeRemotes(fs afero.Fs, repo *git.Repository, v *viper.Viper) error { return err } - if err = extend(v, filepath.Dir(configPath)); err != nil { + if err = extend(fs, v, filepath.Dir(configPath)); err != nil { return err } } @@ -194,12 +212,34 @@ func mergeRemotes(fs afero.Fs, repo *git.Repository, v *viper.Viper) error { } // extend merges all files listed in 'extends' option into the config. -func extend(v *viper.Viper, root string) error { - for i, path := range v.GetStringSlice("extends") { +func extend(fs afero.Fs, v *viper.Viper, root string) error { + return extendRecursive(fs, v, root, make(map[string]struct{})) +} + +// extendRecursive merges extends. +// If extends contain other extends they get merged too. +func extendRecursive(fs afero.Fs, v *viper.Viper, root string, extends map[string]struct{}) error { + for _, path := range v.GetStringSlice("extends") { + if _, contains := extends[path]; contains { + return fmt.Errorf("possible recursion in extends: path %s is specified multiple times", path) + } + extends[path] = struct{}{} + if !filepath.IsAbs(path) { path = filepath.Join(root, path) } - if err := merge(fmt.Sprintf("extend_%d", i), path, v); err != nil { + + extendV := newViper(fs, root) + extendV.SetConfigFile(path) + if err := extendV.ReadInConfig(); err != nil { + return err + } + + if err := extendRecursive(fs, extendV, root, extends); err != nil { + return err + } + + if err := v.MergeConfigMap(extendV.AllSettings()); err != nil { return err } } @@ -210,15 +250,13 @@ func extend(v *viper.Viper, root string) error { // merge merges the configuration using viper builtin MergeInConfig. func merge(name, path string, v *viper.Viper) error { v.SetConfigName(name) - if len(path) > 0 { - v.SetConfigFile(path) - } + v.SetConfigFile(path) return v.MergeInConfig() } -func mergeOne(names []string, path string, v *viper.Viper) error { - for _, name := range names { - err := merge(name, path, v) +func mergeLocal(v *viper.Viper) error { + for _, name := range localConfigNames { + err := merge(name, "", v) if err == nil { break } diff --git a/internal/config/load_test.go b/internal/config/load_test.go index 76f0aa43..af54394e 100644 --- a/internal/config/load_test.go +++ b/internal/config/load_test.go @@ -612,6 +612,64 @@ pre-push: }, }, }, + { + name: "with extends and local", + global: ` +extends: + - global-extend.yml +pre-commit: + parallel: true + exclude_tags: [linter] + commands: + global-lint: + run: bundle exec rubocop + glob: "*.rb" + tags: [backend, linter] + global-other: + run: bundle exec rubocop + tags: [other] +`, + local: ` +pre-commit: + exclude_tags: [backend] +`, + otherFiles: map[string]string{ + "global-extend.yml": ` +pre-commit: + exclude_tags: [test] + commands: + extended-tests: + run: bundle exec rspec + tags: [backend, test] +`, + }, + result: &Config{ + SourceDir: DefaultSourceDir, + SourceDirLocal: DefaultSourceDirLocal, + Extends: []string{"global-extend.yml"}, + Hooks: map[string]*Hook{ + "pre-commit": { + Parallel: true, + ExcludeTags: []string{"backend"}, + Commands: map[string]*Command{ + "global-lint": { + Run: "bundle exec rubocop", + Tags: []string{"backend", "linter"}, + Glob: "*.rb", + }, + "global-other": { + Run: "bundle exec rubocop", + Tags: []string{"other"}, + }, + "extended-tests": { + Run: "bundle exec rspec", + Tags: []string{"backend", "test"}, + }, + }, + }, + }, + }, + }, } { fs := afero.Afero{Fs: afero.NewMemMapFs()} repo := &git.Repository{ diff --git a/internal/git/command_executor.go b/internal/git/command_executor.go index e380dd00..64f12b36 100644 --- a/internal/git/command_executor.go +++ b/internal/git/command_executor.go @@ -71,7 +71,7 @@ func (c CommandExecutor) CmdLinesWithinFolder(cmd []string, folder string) ([]st } func (c CommandExecutor) execute(cmd []string, root string) (string, error) { - out := bytes.NewBuffer(make([]byte, 0)) + out := new(bytes.Buffer) err := c.cmd.Run(cmd, root, system.NullReader, out) strOut := out.String() diff --git a/internal/lefthook/install.go b/internal/lefthook/install.go index 507cac75..9d7a102b 100644 --- a/internal/lefthook/install.go +++ b/internal/lefthook/install.go @@ -5,6 +5,7 @@ import ( "crypto/md5" "encoding/hex" "errors" + "fmt" "io" "os" "path/filepath" @@ -114,18 +115,20 @@ func (l *Lefthook) createConfig(path string) error { return nil } -func (l *Lefthook) syncHooks(cfg *config.Config) (*config.Config, error) { +func (l *Lefthook) syncHooks(cfg *config.Config, fetchRemotes bool) (*config.Config, error) { var remotesSynced bool var err error - for _, remote := range cfg.Remotes { - if remote.Configured() && remote.Refetch { - if err = l.repo.SyncRemote(remote.GitURL, remote.Ref, false); err != nil { - log.Warnf("Couldn't sync from %s. Will continue anyway: %s", remote.GitURL, err) - continue - } + if fetchRemotes { + for _, remote := range cfg.Remotes { + if remote.Configured() && remote.Refetch { + if err = l.repo.SyncRemote(remote.GitURL, remote.Ref, false); err != nil { + log.Warnf("Couldn't sync from %s. Will continue anyway: %s", remote.GitURL, err) + continue + } - remotesSynced = true + remotesSynced = true + } } } @@ -133,12 +136,12 @@ func (l *Lefthook) syncHooks(cfg *config.Config) (*config.Config, error) { // Reread the config file with synced remotes cfg, err = l.readOrCreateConfig() if err != nil { - return nil, err + return nil, fmt.Errorf("failed to reread the config: %w", err) } } // Don't rely on config checksum if remotes were refetched - return cfg, l.createHooksIfNeeded(cfg, !remotesSynced, false) + return cfg, l.createHooksIfNeeded(cfg, true, false) } func (l *Lefthook) createHooksIfNeeded(cfg *config.Config, checkHashSum, force bool) error { @@ -157,11 +160,11 @@ func (l *Lefthook) createHooksIfNeeded(cfg *config.Config, checkHashSum, force b checksum, err := l.configChecksum() if err != nil { - return err + return fmt.Errorf("could not calculate checksum: %w", err) } if err = l.ensureHooksDirExists(); err != nil { - return err + return fmt.Errorf("could not create hooks dir: %w", err) } rootsMap := make(map[string]struct{}) @@ -185,7 +188,7 @@ func (l *Lefthook) createHooksIfNeeded(cfg *config.Config, checkHashSum, force b hookNames = append(hookNames, hook) if err = l.cleanHook(hook, force); err != nil { - return err + return fmt.Errorf("could not replace the hook: %w", err) } templateArgs := templates.Args{ @@ -194,7 +197,7 @@ func (l *Lefthook) createHooksIfNeeded(cfg *config.Config, checkHashSum, force b Roots: roots, } if err = l.addHook(hook, templateArgs); err != nil { - return err + return fmt.Errorf("could not add the hook: %w", err) } } @@ -208,7 +211,7 @@ func (l *Lefthook) createHooksIfNeeded(cfg *config.Config, checkHashSum, force b } if err = l.addChecksumFile(checksum); err != nil { - return err + return fmt.Errorf("could not create a checksum file: %w", err) } success = true @@ -328,7 +331,7 @@ func (l *Lefthook) configChecksum() (checksum string, err error) { func (l *Lefthook) addChecksumFile(checksum string) error { timestamp, err := l.configLastUpdateTimestamp() if err != nil { - return err + return fmt.Errorf("unable to get config update timestamp: %w", err) } return afero.WriteFile( diff --git a/internal/lefthook/lefthook.go b/internal/lefthook/lefthook.go index 6c81317e..b2c8e00c 100644 --- a/internal/lefthook/lefthook.go +++ b/internal/lefthook/lefthook.go @@ -16,8 +16,8 @@ import ( ) const ( + EnvVerbose = "LEFTHOOK_VERBOSE" // keep all output hookFileMode = 0o755 - envVerbose = "LEFTHOOK_VERBOSE" // keep all output oldHookPostfix = ".old" ) @@ -41,7 +41,7 @@ type Lefthook struct { // New returns an instance of Lefthook. func initialize(opts *Options) (*Lefthook, error) { - if os.Getenv(envVerbose) == "1" || os.Getenv(envVerbose) == "true" { + if os.Getenv(EnvVerbose) == "1" || os.Getenv(EnvVerbose) == "true" { opts.Verbose = true } diff --git a/internal/lefthook/run.go b/internal/lefthook/run.go index 2185ccb6..1906574d 100644 --- a/internal/lefthook/run.go +++ b/internal/lefthook/run.go @@ -73,7 +73,9 @@ func (l *Lefthook) Run(hookName string, args RunArgs, gitArgs []string) error { // prepare-commit-msg hook is used for seamless synchronization of hooks with config. // See: internal/lefthook/install.go _, ok := cfg.Hooks[hookName] + var isGhostHook bool if hookName == config.GhostHookName && !ok && !verbose { + isGhostHook = true log.SetLevel(log.WarnLevel) } @@ -97,11 +99,10 @@ func (l *Lefthook) Run(hookName string, args RunArgs, gitArgs []string) error { if !args.NoAutoInstall { // This line controls updating the git hook if config has changed - newCfg, err := l.syncHooks(cfg) + newCfg, err := l.syncHooks(cfg, !isGhostHook) if err != nil { - log.Warn( - `⚠️ There was a problem with synchronizing git hooks. -Run 'lefthook install' manually.`, + log.Warnf( + "⚠️ There was a problem with synchronizing git hooks. Run 'lefthook install' manually.\n Error: %s", err, ) } else { cfg = newCfg @@ -147,6 +148,7 @@ Run 'lefthook install' manually.`, sourceDirs = append( sourceDirs, filepath.Join( + l.repo.RootPath, l.repo.RemoteFolder(remote.GitURL, remote.Ref), cfg.SourceDir, ), diff --git a/internal/lefthook/runner/filters/filters.go b/internal/lefthook/runner/filters/filters.go index b82cfe71..e107012e 100644 --- a/internal/lefthook/runner/filters/filters.go +++ b/internal/lefthook/runner/filters/filters.go @@ -62,18 +62,55 @@ func byGlob(vs []string, matcher string) []string { return vsf } -func byExclude(vs []string, matcher string) []string { - if matcher == "" { +func byExclude(vs []string, matcher interface{}) []string { + switch exclude := matcher.(type) { + case nil: return vs - } + case string: + if len(exclude) == 0 { + return vs + } - vsf := make([]string, 0) - for _, v := range vs { - if res, _ := regexp.MatchString(matcher, v); !res { - vsf = append(vsf, v) + vsf := make([]string, 0) + for _, v := range vs { + if res, _ := regexp.MatchString(exclude, v); !res { + vsf = append(vsf, v) + } + } + + return vsf + case []interface{}: + if len(exclude) == 0 { + return vs + } + + globs := make([]glob.Glob, 0, len(exclude)) + for _, name := range exclude { + globs = append(globs, glob.MustCompile(name.(string))) + } + + var foundMatch bool + vsf := make([]string, 0) + for _, v := range vs { + for _, g := range globs { + if ok := g.Match(v); ok { + foundMatch = true + break + } + } + + if !foundMatch { + vsf = append(vsf, v) + } + foundMatch = false } + + return vsf } - return vsf + + log.Warn("invalid value for exclude option") + + return vs } func byRoot(vs []string, matcher string) []string { diff --git a/internal/lefthook/runner/runner.go b/internal/lefthook/runner/runner.go index 312219fa..622a8a0b 100644 --- a/internal/lefthook/runner/runner.go +++ b/internal/lefthook/runner/runner.go @@ -145,7 +145,7 @@ func (r *Runner) runLFSHook(ctx context.Context) error { log.Debugf( "[git-lfs] executing hook: git lfs %s %s", r.HookName, strings.Join(r.GitArgs, " "), ) - out := bytes.NewBuffer(make([]byte, 0)) + out := new(bytes.Buffer) err := r.cmd.RunWithContext( ctx, append( @@ -521,7 +521,7 @@ func (r *Runner) run(ctx context.Context, opts exec.Options, follow bool) bool { return err == nil } - out := bytes.NewBuffer(make([]byte, 0)) + out := new(bytes.Buffer) err := r.executor.Execute(ctx, opts, in, out) diff --git a/internal/templates/hook.tmpl b/internal/templates/hook.tmpl index 0a6e1835..676edaf2 100644 --- a/internal/templates/hook.tmpl +++ b/internal/templates/hook.tmpl @@ -29,57 +29,61 @@ call_lefthook() {{end -}} else dir="$(git rev-parse --show-toplevel)" - if test -f "$dir/node_modules/lefthook/bin/index.js" + osArch=$(uname | tr '[:upper:]' '[:lower:]') + cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/') + if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook{{.Extension}}" + then + "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook{{.Extension}}" "$@" + elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook{{.Extension}}" + then + "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook{{.Extension}}" "$@" + elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook{{.Extension}}" + then + "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook{{.Extension}}" "$@" + elif test -f "$dir/node_modules/lefthook/bin/index.js" then "$dir/node_modules/lefthook/bin/index.js" "$@" + {{ $extension := .Extension }} + {{- range .Roots -}} + elif test -f "$dir/{{.}}/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook{{$extension}}" + then + "$dir/{{.}}/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook{{$extension}}" "$@" + elif test -f "$dir/{{.}}/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook{{$extension}}" + then + "$dir/{{.}}/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook{{$extension}}" "$@" + elif test -f "$dir/{{.}}/node_modules/@evilmartians/lefthook-installer/bin/lefthook{{$extension}}" + then + "$dir/{{.}}/node_modules/@evilmartians/lefthook-installer/bin/lefthook{{$extension}}" "$@" + elif test -f "$dir/{{.}}/node_modules/lefthook/bin/index.js" + then + "$dir/{{.}}/node_modules/lefthook/bin/index.js" "$@" + {{end}} + elif bundle exec lefthook -h >/dev/null 2>&1 + then + bundle exec lefthook "$@" + elif yarn lefthook -h >/dev/null 2>&1 + then + yarn lefthook "$@" + elif pnpm lefthook -h >/dev/null 2>&1 + then + pnpm lefthook "$@" + elif swift package plugin lefthook >/dev/null 2>&1 + then + swift package --disable-sandbox plugin lefthook "$@" + elif command -v mint >/dev/null 2>&1 + then + mint run csjones/lefthook-plugin "$@" + elif command -v npx >/dev/null 2>&1 + then + npx lefthook "$@" else - osArch=$(uname | tr '[:upper:]' '[:lower:]') - cpuArch=$(uname -m | sed 's/aarch64/arm64/') - if test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook{{.Extension}}" - then - "$dir/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook{{.Extension}}" "$@" - elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook{{.Extension}}" - then - "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook{{.Extension}}" "$@" - {{ $extension := .Extension }} - {{- range .Roots -}} - elif test -f "$dir/{{.}}/node_modules/lefthook/bin/index.js" - then - "$dir/{{.}}/node_modules/lefthook/bin/index.js" "$@" - elif test -f "$dir/{{.}}/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook{{$extension}}" - then - "$dir/{{.}}/node_modules/@evilmartians/lefthook/bin/lefthook_${osArch}_${cpuArch}/lefthook{{$extension}}" "$@" - elif test -f "$dir/{{.}}/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook{{$extension}}" - then - "$dir/{{.}}/node_modules/@evilmartians/lefthook-installer/bin/lefthook_${osArch}_${cpuArch}/lefthook{{$extension}}" "$@" - {{end}} - elif bundle exec lefthook -h >/dev/null 2>&1 - then - bundle exec lefthook "$@" - elif yarn lefthook -h >/dev/null 2>&1 - then - yarn lefthook "$@" - elif pnpm lefthook -h >/dev/null 2>&1 - then - pnpm lefthook "$@" - elif swift package plugin lefthook >/dev/null 2>&1 - then - swift package --disable-sandbox plugin lefthook "$@" - elif command -v mint >/dev/null 2>&1 - then - mint run csjones/lefthook-plugin "$@" - elif command -v npx >/dev/null 2>&1 - then - npx lefthook "$@" - else - echo "Can't find lefthook in PATH" - {{- if .AssertLefthookInstalled}} - echo "ERROR: Operation is aborted due to lefthook settings." - echo "Make sure lefthook is available in your environment and re-try." - echo "To skip these checks use --no-verify git argument or set LEFTHOOK=0 env variable." - exit 1 - {{- end}} - fi + echo "Can't find lefthook in PATH" + {{- if .AssertLefthookInstalled}} + echo "ERROR: Operation is aborted due to lefthook settings." + echo "Make sure lefthook is available in your environment and re-try." + echo "To skip these checks use --no-verify git argument or set LEFTHOOK=0 env variable." + exit 1 + {{- end}} fi fi } diff --git a/internal/updater/updater.go b/internal/updater/updater.go new file mode 100644 index 00000000..c201bfa4 --- /dev/null +++ b/internal/updater/updater.go @@ -0,0 +1,278 @@ +// Package updater contains the self-update implementation for the lefthook executable. +package updater + +import ( + "bufio" + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/schollz/progressbar/v3" + + "github.com/evilmartians/lefthook/internal/log" + "github.com/evilmartians/lefthook/internal/version" +) + +const ( + timeout = 10 * time.Second + latestReleaseURL = "https://api.github.com/repos/evilmartians/lefthook/releases/latest" + checksumsFilename = "lefthook_checksums.txt" + checksumFields = 2 + modExecutable os.FileMode = 0o755 +) + +var ( + errNoAsset = errors.New("Couldn't find an asset to download. Please submit an issue to https://github.com/evilmartians/lefthook") + errInvalidHashsum = errors.New("SHA256 sums differ, it's not safe to use the downloaded binary.\nIf you have problems upgrading lefthook please submit an issue to https://github.com/evilmartians/lefthook") + errUpdateFailed = errors.New("Update failed") + + osNames = map[string]string{ + "windows": "Windows", + "darwin": "MacOS", + "linux": "Linux", + "freebsd": "Freebsd", + } + + archNames = map[string]string{ + "amd64": "x86_64", + "arm64": "arm64", + "386": "i386", + } +) + +type release struct { + TagName string `json:"tag_name"` + Assets []asset +} + +type asset struct { + Name string `json:"name"` + DownloadURL string `json:"browser_download_url"` +} + +type Options struct { + Yes bool + Force bool + ExePath string +} + +type Updater struct { + client *http.Client + releaseURL string +} + +func New() *Updater { + return &Updater{ + client: &http.Client{Timeout: timeout}, + releaseURL: latestReleaseURL, + } +} + +func (u *Updater) SelfUpdate(ctx context.Context, opts Options) error { + rel, ferr := u.fetchLatestRelease(ctx) + if ferr != nil { + return fmt.Errorf("latest release fetch failed: %w", ferr) + } + + latestVersion := strings.TrimPrefix(rel.TagName, "v") + + if latestVersion == version.Version(false) && !opts.Force { + log.Infof("Up to date: %s\n", latestVersion) + return nil + } + + wantedAsset := fmt.Sprintf("lefthook_%s_%s_%s", latestVersion, osNames[runtime.GOOS], archNames[runtime.GOARCH]) + if runtime.GOOS == "windows" { + wantedAsset += ".exe" + } + + log.Debugf("Searching assets for %s", wantedAsset) + + var downloadURL string + var checksumURL string + for i := range rel.Assets { + asset := rel.Assets[i] + if len(downloadURL) == 0 && asset.Name == wantedAsset { + downloadURL = asset.DownloadURL + if len(checksumURL) > 0 { + break + } + } + + if len(checksumURL) == 0 && asset.Name == checksumsFilename { + checksumURL = asset.DownloadURL + if len(downloadURL) > 0 { + break + } + } + } + + if len(downloadURL) == 0 { + log.Warnf("Couldn't find the right asset to download. Wanted: %s\n", wantedAsset) + return errNoAsset + } + + if len(checksumURL) == 0 { + log.Warn("Couldn't find checksums") + } + + if !opts.Yes { + log.Infof("Update %s to %s? %s ", log.Cyan("lefthook"), log.Yellow(latestVersion), log.Gray("[Y/n]")) + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + ans := scanner.Text() + + if len(ans) > 0 && ans[0] != 'y' && ans[0] != 'Y' { + log.Debug("Update rejected") + return nil + } + } + + lefthookExePath := opts.ExePath + if realPath, serr := filepath.EvalSymlinks(lefthookExePath); serr == nil { + lefthookExePath = realPath + } + + destPath := lefthookExePath + "." + latestVersion + defer os.Remove(destPath) + + ok, err := u.download(ctx, wantedAsset, downloadURL, checksumURL, destPath) + if err != nil { + return err + } + if !ok { + return errInvalidHashsum + } + + backupPath := lefthookExePath + ".bak" + defer os.Remove(backupPath) + + log.Debugf("mv %s %s", lefthookExePath, backupPath) + if err = os.Rename(lefthookExePath, backupPath); err != nil { + return fmt.Errorf("failed to backup lefthook executable: %w", err) + } + + log.Debugf("mv %s %s", destPath, lefthookExePath) + err = os.Rename(destPath, lefthookExePath) + if err != nil { + log.Errorf("Failed to replace the lefthook executable: %s", err) + if err = os.Rename(backupPath, lefthookExePath); err != nil { + return fmt.Errorf("failed to recover from backup: %w", err) + } + + return errUpdateFailed + } + + log.Debugf("chmod +x %s", lefthookExePath) + if err = os.Chmod(lefthookExePath, modExecutable); err != nil { + log.Errorf("Failed to set executable file mode: %s", err) + if err = os.Rename(backupPath, lefthookExePath); err != nil { + return fmt.Errorf("failed to recover from backup: %w", err) + } + + return errUpdateFailed + } + + return nil +} + +func (u *Updater) fetchLatestRelease(ctx context.Context) (*release, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.releaseURL, nil) + if err != nil { + return nil, fmt.Errorf("failed to initialize a request: %w", err) + } + req.Header.Set("Accept", "application/vnd.github+json") + req.Header.Set("X-GitHub-Api-Version", "2022-11-28") + + resp, err := u.client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + var rel release + if err = json.NewDecoder(resp.Body).Decode(&rel); err != nil { + return nil, fmt.Errorf("failed to parse the Github response: %w", err) + } + + return &rel, nil +} + +func (u *Updater) download(ctx context.Context, name, fileURL, checksumURL, path string) (bool, error) { + log.Debugf("Downloading %s to %s", fileURL, path) + + filereq, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil) + if err != nil { + return false, fmt.Errorf("failed to build download request: %w", err) + } + + sumreq, err := http.NewRequestWithContext(ctx, http.MethodGet, checksumURL, nil) + if err != nil { + return false, fmt.Errorf("failed to build checksum download request: %w", err) + } + + file, err := os.Create(path) + if err != nil { + return false, fmt.Errorf("failed to create destination path (%s): %w", path, err) + } + defer file.Close() + + resp, err := u.client.Do(filereq) + if err != nil { + return false, fmt.Errorf("download request failed: %w", err) + } + defer resp.Body.Close() + + checksumResp, err := u.client.Do(sumreq) + if err != nil { + return false, fmt.Errorf("checksum download request failed: %w", err) + } + defer checksumResp.Body.Close() + + bar := progressbar.DefaultBytes(resp.ContentLength+checksumResp.ContentLength, name) + + fileHasher := sha256.New() + if _, err = io.Copy(io.MultiWriter(file, fileHasher, bar), resp.Body); err != nil { + return false, fmt.Errorf("failed to download the file: %w", err) + } + log.Debug() + + hashsum := hex.EncodeToString(fileHasher.Sum(nil)) + + scanner := bufio.NewScanner(checksumResp.Body) + for scanner.Scan() { + sums := strings.Fields(scanner.Text()) + if len(sums) < checksumFields { + continue + } + + log.Debugf("Checking %s %s", sums[0], sums[1]) + if sums[1] == name { + if sums[0] == hashsum { + if err = bar.Finish(); err != nil { + log.Debugf("Progressbar error: %s", err) + } + + log.Debugf("Match %s %s", sums[0], sums[1]) + + return true, nil + } else { + return false, nil + } + } + } + + log.Debugf("No matches found for %s %s", name, hashsum) + + return false, nil +} diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go new file mode 100644 index 00000000..0f3e78e2 --- /dev/null +++ b/internal/updater/updater_test.go @@ -0,0 +1,162 @@ +package updater + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/evilmartians/lefthook/internal/version" +) + +func TestUpdater_SelfUpdate(t *testing.T) { + var extension string + if runtime.GOOS == "windows" { + extension = ".exe" + } + exePath := filepath.Join(os.TempDir(), "lefthook") + for name, tt := range map[string]struct { + latestRelease string + assetName string + checksums string + opts Options + asset []byte + err error + }{ + "asset not found": { + latestRelease: "v1.0.0", + assetName: "lefthook_1.0.0_darwin_arm64", + opts: Options{ + Yes: true, + Force: false, + ExePath: exePath, + }, + err: errNoAsset, + }, + "no need to update": { + latestRelease: "v" + version.Version(false), + assetName: "lefthook_1.0.0_darwin_arm64", + opts: Options{ + Yes: true, + Force: false, + ExePath: exePath, + }, + err: nil, + }, + "forced update but asset not found": { + latestRelease: "v" + version.Version(false), + assetName: "lefthook_1.0.0_darwin_arm64", + opts: Options{ + Yes: true, + Force: true, + ExePath: exePath, + }, + err: errNoAsset, + }, + "invalid hashsum": { + latestRelease: "v1.0.0", + assetName: "lefthook_1.0.0_" + osNames[runtime.GOOS] + "_" + archNames[runtime.GOARCH] + extension, + opts: Options{ + Yes: true, + Force: true, + ExePath: exePath, + }, + asset: []byte{65, 54, 24, 32, 43, 67, 21}, + checksums: ` + 67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_MacOS_arm64 + 67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_MacOS_x86_64 + 67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_Linux_x86_64 + 67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_Linux_arm64 + 67a5740c6c66d986c5708cddd6bd0bc240db29451646fc4c1398b988dcf7cdfe lefthook_1.0.0_Windows_x86_64.exe + `, + err: errInvalidHashsum, + }, + "success": { + latestRelease: "v1.0.0", + assetName: "lefthook_1.0.0_" + osNames[runtime.GOOS] + "_" + archNames[runtime.GOARCH] + extension, + opts: Options{ + Yes: true, + Force: true, + ExePath: exePath, + }, + asset: []byte{65, 54, 24, 32, 43, 67, 21}, + checksums: ` + 0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_MacOS_arm64 + 0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_MacOS_x86_64 + 0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_Linux_x86_64 + 0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_Linux_arm64 + 0e1c97246ba1bc8bde78355ae986589545d3c69bf1264d2d3c1835ec072006f6 lefthook_1.0.0_Windows_x86_64.exe + `, + err: nil, + }, + } { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + file, err := os.Create(tt.opts.ExePath) + assert.NoError(err) + file.Close() + + checksumServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n, werr := w.Write([]byte(tt.checksums)) + assert.Equal(n, len(tt.checksums)) + assert.NoError(werr) + })) + defer checksumServer.Close() + assetServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n, werr := w.Write(tt.asset) + assert.Equal(n, len(tt.asset)) + assert.NoError(werr) + })) + defer assetServer.Close() + + releaseServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.NoError(json.NewEncoder(w).Encode(map[string]interface{}{ + "tag_name": tt.latestRelease, + "assets": []map[string]string{ + { + "name": tt.assetName, + "browser_download_url": assetServer.URL, + }, + { + "name": "lefthook_checksums.txt", + "browser_download_url": checksumServer.URL, + }, + }, + })) + })) + defer releaseServer.Close() + + upd := Updater{ + client: releaseServer.Client(), + releaseURL: releaseServer.URL, + } + + err = upd.SelfUpdate(context.Background(), tt.opts) + + if tt.err != nil { + if !errors.Is(err, tt.err) { + t.Error(err) + } + } else { + assert.NoError(err) + + if tt.asset != nil { + content, err := os.ReadFile(tt.opts.ExePath) + assert.NoError(err) + + assert.Equal(content, tt.asset) + } + } + }) + } +} diff --git a/internal/version/version.go b/internal/version/version.go index b543e859..ff68ad10 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -8,7 +8,7 @@ import ( "strconv" ) -const version = "1.6.14" +const version = "1.7.9" var ( // Is set via -X github.com/evilmartians/lefthook/internal/version.commit={commit}. diff --git a/packaging/Makefile b/packaging/Makefile deleted file mode 100644 index c79fe801..00000000 --- a/packaging/Makefile +++ /dev/null @@ -1,52 +0,0 @@ -# Packages version to release -VERSION := 1.6.14 - -DIST_DIR := ../dist - -LINUX_AMD64_BIN=$(DIST_DIR)/lefthook_linux_amd64_v1/lefthook -LINUX_ARM64_BIN=$(DIST_DIR)/lefthook_linux_arm64/lefthook -FREEBSD_AMD64_BIN=$(DIST_DIR)/lefthook_freebsd_amd64_v1/lefthook -FREEBSD_ARM64_BIN=$(DIST_DIR)/lefthook_freebsd_arm64/lefthook -WINDOWS_AMD64_BIN=$(DIST_DIR)/lefthook_windows_amd64_v1/lefthook.exe -WINDOWS_ARM64_BIN=$(DIST_DIR)/lefthook_windows_arm64/lefthook.exe -DARWIN_AMD64_BIN=$(DIST_DIR)/lefthook_darwin_amd64_v1/lefthook -DARWIN_ARM64_BIN=$(DIST_DIR)/lefthook_darwin_arm64/lefthook - -prepare: clean set-version put-readme put-binaries - -publish: - cd npm; find . -type d -name 'lefthook*' -exec npm publish --access public \{} \; ; cd - - cd npm-bundled; npm publish --access public ; cd - - cd npm-installer; npm publish --access public ; cd - - cd rubygems; rake build; gem push pkg/*.gem ; cd - - -# Update versions of all packages -set-version: - find npm -name 'package.json' -type f -print0 | xargs -0 sed -E -i "s/\"version\": \".+\"/\"version\": \"$(VERSION)\"/" - sed -E -i "s/\"(lefthook-.+)\": \".+\"/\"\1\": \"$(VERSION)\"/g" npm/lefthook/package.json - sed -E -i "0,/version/{s/\"version\": \".+\"/\"version\": \"$(VERSION)\"/}" npm-bundled/package.json - sed -E -i "0,/version/{s/\"version\": \".+\"/\"version\": \"$(VERSION)\"/}" npm-installer/package.json - sed -E -i "s/(spec\.version\s+= ).*/\1\"$(VERSION)\"/" rubygems/lefthook.gemspec - -put-binaries: - install -D $(LINUX_AMD64_BIN) npm/lefthook-linux-x64/bin/lefthook - install -D $(LINUX_ARM64_BIN) npm/lefthook-linux-arm64/bin/lefthook - install -D $(FREEBSD_AMD64_BIN) npm/lefthook-freebsd-x64/bin/lefthook - install -D $(FREEBSD_ARM64_BIN) npm/lefthook-freebsd-arm64/bin/lefthook - install -D $(WINDOWS_AMD64_BIN) npm/lefthook-windows-x64/bin/lefthook.exe - install -D $(WINDOWS_ARM64_BIN) npm/lefthook-windows-arm64/bin/lefthook.exe - install -D $(DARWIN_AMD64_BIN) npm/lefthook-darwin-x64/bin/lefthook - install -D $(DARWIN_ARM64_BIN) npm/lefthook-darwin-arm64/bin/lefthook - cd npm-bundled; npm version $(VERSION) --allow-same-version; cd - - cd $(DIST_DIR); find . -maxdepth 2 -type f -exec cp --parents \{\} ../packaging/rubygems/libexec/ \; - -put-readme: - find npm/ -type d -name 'lefthook*' -exec cp -f ../README.md \{} \; - cp ../README.md npm-bundled/ - cp ../README.md npm-installer/ - -clean: - find npm/ -name 'README.md' -exec rm \{} \; - find npm/ -type f -name 'lefthook*' -exec rm \{} \; - git clean -fdX npm-installer/ npm-bundled/ npm-bundled/bin/ - git clean -fdX rubygems/libexec/ rubygems/pkg/ diff --git a/packaging/npm-bundled/get-exe.js b/packaging/npm-bundled/get-exe.js index a3a8de0a..192d335d 100644 --- a/packaging/npm-bundled/get-exe.js +++ b/packaging/npm-bundled/get-exe.js @@ -15,11 +15,6 @@ function getExePath() { let goArch = process.arch; let suffix = ''; switch (process.arch) { - case 'x64': { - goArch = 'amd64'; - suffix = '_v1'; // GOAMD64 - break; - } case 'x32': case 'ia32': { goArch = '386'; @@ -30,7 +25,7 @@ function getExePath() { const dir = path.join(__dirname, 'bin'); const executable = path.join( dir, - `lefthook_${goOS}_${goArch}${suffix}`, + `lefthook-${goOS}-${goArch}`, `lefthook${extension}` ); return executable; diff --git a/packaging/npm-bundled/package.json b/packaging/npm-bundled/package.json index 0a25599e..6f12ff2f 100644 --- a/packaging/npm-bundled/package.json +++ b/packaging/npm-bundled/package.json @@ -1,12 +1,15 @@ { "name": "@evilmartians/lefthook", - "version": "1.6.14", + "version": "1.7.9", "description": "Simple git hooks manager", - "main": "index.js", + "main": "bin/index.js", "bin": { - "lefthook": "./bin/index.js" + "lefthook": "bin/index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" }, - "repository": "https://github.com/evilmartians/lefthook", "keywords": [ "git", "hook", @@ -30,7 +33,6 @@ "ia32" ], "scripts": { - "version": "git clean -fdX bin/ && (cd ../../dist/ && find . -maxdepth 2 -executable -type f -exec cp --parents \\{\\} ../packaging/npm-bundled/bin/ \\;) && cp -f ../../README.md ./", "postinstall": "node postinstall.js" } } diff --git a/packaging/npm-installer/install.js b/packaging/npm-installer/install.js index ff823f55..f0ed1946 100755 --- a/packaging/npm-installer/install.js +++ b/packaging/npm-installer/install.js @@ -1,4 +1,7 @@ -const { spawnSync } = require("child_process") +const http = require('https') +const fs = require('fs') +const path = require("path") +const chp = require("child_process") const iswin = ["win32", "cygwin"].includes(process.platform) @@ -6,15 +9,19 @@ async function install() { if (process.env.CI) { return } - const exePath = await downloadBinary() + const downloadURL = getDownloadURL() + const extension = iswin ? ".exe" : "" + const fileName = `lefthook${extension}` + const exePath = path.join(__dirname, "bin", fileName) + await downloadBinary(downloadURL, exePath) + console.log('downloaded to', exePath) if (!iswin) { - const { chmodSync } = require("fs") - chmodSync(exePath, "755") + fs.chmodSync(exePath, "755") } // run install - spawnSync(exePath, ["install", "-f"], { + chp.spawnSync(exePath, ['install', '-f'], { cwd: process.env.INIT_CWD || process.cwd(), - stdio: "inherit", + stdio: 'inherit', }) } @@ -46,28 +53,31 @@ function getDownloadURL() { return `https://github.com/evilmartians/lefthook/releases/download/v${version}/lefthook_${version}_${downloadOS}_${arch}${extension}` } -const { DownloaderHelper } = require("node-downloader-helper") -const path = require("path") +async function downloadBinary(url, dest) { + console.log('downloading', url) + const file = fs.createWriteStream(dest) + return new Promise((resolve, reject) => { + http.get(url, function(response) { + if (response.statusCode === 302 && response.headers.location) { + // If the response is a 302 redirect, follow the new location + downloadBinary(response.headers.location, dest) + .then(resolve) + .catch(reject) + } else { + response.pipe(file) -async function downloadBinary() { - // TODO zip the binaries to reduce the download size - const downloadURL = getDownloadURL() - const extension = iswin ? ".exe" : "" - const fileName = `lefthook${extension}` - const binDir = path.join(__dirname, "bin") - const dl = new DownloaderHelper(downloadURL, binDir, { - fileName, - retry: { maxRetries: 5, delay: 50 }, + file.on('finish', function() { + file.close(() => { + resolve(dest) + }) + }) + } + }).on('error', function(err) { + fs.unlink(file, () => { + reject(err) + }) + }) }) - dl.on("end", () => console.log("lefthook binary was downloaded")) - try { - await dl.start() - } catch(e) { - const message = `Failed to download ${fileName}: ${e.message} while fetching ${downloadURL}` - console.error(message) - throw new Error(message) - } - return path.join(binDir, fileName) } // start: diff --git a/packaging/npm-installer/package.json b/packaging/npm-installer/package.json index 826d58c7..dfc4163d 100644 --- a/packaging/npm-installer/package.json +++ b/packaging/npm-installer/package.json @@ -1,12 +1,15 @@ { "name": "@evilmartians/lefthook-installer", - "version": "1.6.14", + "version": "1.7.9", "description": "Simple git hooks manager", - "main": "index.js", + "main": "bin/index.js", "bin": { - "lefthook": "./bin/index.js" + "lefthook": "bin/index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" }, - "repository": "https://github.com/evilmartians/lefthook", "keywords": [ "git", "hook", @@ -29,10 +32,6 @@ "arm64" ], "scripts": { - "version": "cp -f ../../README.md ./", "install": "node install.js" - }, - "dependencies": { - "node-downloader-helper": "^1.0.18" } } diff --git a/packaging/npm/lefthook-darwin-arm64/package.json b/packaging/npm/lefthook-darwin-arm64/package.json index c2c55909..59e885bb 100644 --- a/packaging/npm/lefthook-darwin-arm64/package.json +++ b/packaging/npm/lefthook-darwin-arm64/package.json @@ -1,9 +1,15 @@ { "name": "lefthook-darwin-arm64", - "version": "1.6.14", + "version": "1.7.9", "description": "The macOS ARM 64-bit binary for lefthook, git hooks manager.", "preferUnplugged": false, - "repository": "https://github.com/evilmartians/lefthook", + "bin": { + "lefthook": "bin/lefthook" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" + }, "license": "MIT", "bugs": { "url": "https://github.com/evilmartians/lefthook/issues", diff --git a/packaging/npm/lefthook-darwin-x64/package.json b/packaging/npm/lefthook-darwin-x64/package.json index 55665b43..f729cb7f 100644 --- a/packaging/npm/lefthook-darwin-x64/package.json +++ b/packaging/npm/lefthook-darwin-x64/package.json @@ -1,9 +1,15 @@ { "name": "lefthook-darwin-x64", - "version": "1.6.14", + "version": "1.7.9", "description": "The macOS 64-bit binary for lefthook, git hooks manager.", "preferUnplugged": false, - "repository": "https://github.com/evilmartians/lefthook", + "bin": { + "lefthook": "bin/lefthook" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" + }, "license": "MIT", "bugs": { "url": "https://github.com/evilmartians/lefthook/issues", diff --git a/packaging/npm/lefthook-freebsd-arm64/package.json b/packaging/npm/lefthook-freebsd-arm64/package.json index d6ecf8c0..04df9c32 100644 --- a/packaging/npm/lefthook-freebsd-arm64/package.json +++ b/packaging/npm/lefthook-freebsd-arm64/package.json @@ -1,9 +1,15 @@ { "name": "lefthook-freebsd-arm64", - "version": "1.6.14", + "version": "1.7.9", "description": "The FreeBSD ARM 64-bit binary for lefthook, git hooks manager.", "preferUnplugged": false, - "repository": "https://github.com/evilmartians/lefthook", + "bin": { + "lefthook": "bin/lefthook" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" + }, "license": "MIT", "bugs": { "url": "https://github.com/evilmartians/lefthook/issues", diff --git a/packaging/npm/lefthook-freebsd-x64/package.json b/packaging/npm/lefthook-freebsd-x64/package.json index ce049d9d..f92f3d17 100644 --- a/packaging/npm/lefthook-freebsd-x64/package.json +++ b/packaging/npm/lefthook-freebsd-x64/package.json @@ -1,9 +1,15 @@ { "name": "lefthook-freebsd-x64", - "version": "1.6.14", + "version": "1.7.9", "description": "The FreeBSD 64-bit binary for lefthook, git hooks manager.", "preferUnplugged": false, - "repository": "https://github.com/evilmartians/lefthook", + "bin": { + "lefthook": "bin/lefthook" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" + }, "license": "MIT", "bugs": { "url": "https://github.com/evilmartians/lefthook/issues", diff --git a/packaging/npm/lefthook-linux-arm64/package.json b/packaging/npm/lefthook-linux-arm64/package.json index b26208ee..3549c334 100644 --- a/packaging/npm/lefthook-linux-arm64/package.json +++ b/packaging/npm/lefthook-linux-arm64/package.json @@ -1,9 +1,15 @@ { "name": "lefthook-linux-arm64", - "version": "1.6.14", + "version": "1.7.9", "description": "The Linux ARM 64-bit binary for lefthook, git hooks manager.", "preferUnplugged": false, - "repository": "https://github.com/evilmartians/lefthook", + "bin": { + "lefthook": "bin/lefthook" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" + }, "license": "MIT", "bugs": { "url": "https://github.com/evilmartians/lefthook/issues", diff --git a/packaging/npm/lefthook-linux-x64/package.json b/packaging/npm/lefthook-linux-x64/package.json index 8b925097..6baf0dba 100644 --- a/packaging/npm/lefthook-linux-x64/package.json +++ b/packaging/npm/lefthook-linux-x64/package.json @@ -1,9 +1,15 @@ { "name": "lefthook-linux-x64", - "version": "1.6.14", + "version": "1.7.9", "description": "The Linux 64-bit binary for lefthook, git hooks manager.", "preferUnplugged": false, - "repository": "https://github.com/evilmartians/lefthook", + "bin": { + "lefthook": "bin/lefthook" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" + }, "license": "MIT", "bugs": { "url": "https://github.com/evilmartians/lefthook/issues", diff --git a/packaging/npm/lefthook-windows-arm64/package.json b/packaging/npm/lefthook-windows-arm64/package.json index 1d622f7f..b1a4ac80 100644 --- a/packaging/npm/lefthook-windows-arm64/package.json +++ b/packaging/npm/lefthook-windows-arm64/package.json @@ -1,9 +1,15 @@ { "name": "lefthook-windows-arm64", - "version": "1.6.14", + "version": "1.7.9", "description": "The Windows ARM 64-bit binary for lefthook, git hooks manager.", "preferUnplugged": false, - "repository": "https://github.com/evilmartians/lefthook", + "bin": { + "lefthook": "bin/lefthook" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" + }, "license": "MIT", "bugs": { "url": "https://github.com/evilmartians/lefthook/issues", diff --git a/packaging/npm/lefthook-windows-x64/package.json b/packaging/npm/lefthook-windows-x64/package.json index 0d38ceb8..e21a0fe2 100644 --- a/packaging/npm/lefthook-windows-x64/package.json +++ b/packaging/npm/lefthook-windows-x64/package.json @@ -1,9 +1,15 @@ { "name": "lefthook-windows-x64", - "version": "1.6.14", + "version": "1.7.9", "description": "The Windows 64-bit binary for lefthook, git hooks manager.", "preferUnplugged": false, - "repository": "https://github.com/evilmartians/lefthook", + "bin": { + "lefthook": "bin/lefthook" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" + }, "license": "MIT", "bugs": { "url": "https://github.com/evilmartians/lefthook/issues", diff --git a/packaging/npm/lefthook/get-exe.js b/packaging/npm/lefthook/get-exe.js index c67258ec..ddf8f234 100644 --- a/packaging/npm/lefthook/get-exe.js +++ b/packaging/npm/lefthook/get-exe.js @@ -1,20 +1,20 @@ -const path = require("path") +const path = require("path"); function getExePath() { // Detect OS // https://nodejs.org/api/process.html#process_process_platform let os = process.platform; - let extension = ''; - if (['win32', 'cygwin'].includes(process.platform)) { - os = 'windows'; - extension = '.exe'; + let extension = ""; + if (["win32", "cygwin"].includes(process.platform)) { + os = "windows"; + extension = ".exe"; } // Detect architecture // https://nodejs.org/api/process.html#process_process_arch let arch = process.arch; - return require.resolve(`lefthook-${os}-${arch}/bin/lefthook${extension}`) + return require.resolve(`lefthook-${os}-${arch}/bin/lefthook${extension}`); } exports.getExePath = getExePath; diff --git a/packaging/npm/lefthook/package.json b/packaging/npm/lefthook/package.json index a50876c8..689ce23e 100644 --- a/packaging/npm/lefthook/package.json +++ b/packaging/npm/lefthook/package.json @@ -1,12 +1,12 @@ { "name": "lefthook", - "version": "1.6.14", + "version": "1.7.9", "description": "Simple git hooks manager", - "main": "index.js", - "repository": "https://github.com/evilmartians/lefthook", - "bin": { - "lefthook": "bin/index.js" + "repository": { + "type": "git", + "url": "git+https://github.com/evilmartians/lefthook.git" }, + "main": "bin/index.js", "keywords": [ "git", "hook", @@ -20,14 +20,14 @@ }, "homepage": "https://github.com/evilmartians/lefthook#readme", "optionalDependencies": { - "lefthook-darwin-arm64": "1.6.14", - "lefthook-darwin-x64": "1.6.14", - "lefthook-linux-arm64": "1.6.14", - "lefthook-linux-x64": "1.6.14", - "lefthook-freebsd-arm64": "1.6.14", - "lefthook-freebsd-x64": "1.6.14", - "lefthook-windows-arm64": "1.6.14", - "lefthook-windows-x64": "1.6.14" + "lefthook-darwin-arm64": "1.7.9", + "lefthook-darwin-x64": "1.7.9", + "lefthook-linux-arm64": "1.7.9", + "lefthook-linux-x64": "1.7.9", + "lefthook-freebsd-arm64": "1.7.9", + "lefthook-freebsd-x64": "1.7.9", + "lefthook-windows-arm64": "1.7.9", + "lefthook-windows-x64": "1.7.9" }, "scripts": { "postinstall": "node postinstall.js" diff --git a/packaging/npm/lefthook/postinstall.js b/packaging/npm/lefthook/postinstall.js index 200e7561..23e01eb8 100644 --- a/packaging/npm/lefthook/postinstall.js +++ b/packaging/npm/lefthook/postinstall.js @@ -1,19 +1,21 @@ -const { spawnSync } = require("child_process") -const { getExePath } = require("./get-exe") +const { spawnSync } = require("child_process"); +const { getExePath } = require("./get-exe"); function install() { if (process.env.CI) { - return + return; } spawnSync(getExePath(), ["install", "-f"], { cwd: process.env.INIT_CWD || process.cwd(), stdio: "inherit", - }) + }); } try { - install() -} catch(e) { - console.warn("'lefthook install' command failed. Try running it manually.\n" + e) + install(); +} catch (e) { + console.warn( + "'lefthook install' command failed. Try running it manually.\n" + e, + ); } diff --git a/packaging/pack.rb b/packaging/pack.rb new file mode 100755 index 00000000..84635693 --- /dev/null +++ b/packaging/pack.rb @@ -0,0 +1,140 @@ +#!/usr/bin/env ruby + +require "fileutils" + +VERSION = "1.7.9" + +ROOT = File.join(__dir__, "..") +DIST = File.join(ROOT, "dist") + +module Pack + extend FileUtils + + module_function + + def prepare + clean + set_version + put_readme + put_binaries + end + + def clean + cd(__dir__) + puts "Cleaning... " + rm(Dir["npm/**/README.md"]) + rm(Dir["npm/**/lefthook*"].filter(&File.method(:file?))) + system("git clean -fdX npm-installer/ npm-bundled/ npm-bundled/bin/ rubygems/libexec/ rubygems/pkg/", exception: true) + puts "done" + end + + def set_version + cd(__dir__) + puts "Replacing version to #{VERSION} in packages" + (Dir["npm/**/package.json"] + ["npm-bundled/package.json", "npm-installer/package.json"]).each do |package_json| + replace_in_file(package_json, /"version": "[\d.]+"/, %{"version": "#{VERSION}"}) + end + + replace_in_file("npm/lefthook/package.json", /"(lefthook-.+)": "[\d.]+"/, %{"\\1": "#{VERSION}"}) + replace_in_file("rubygems/lefthook.gemspec", /(spec\.version\s+= ).*/, %{\\1"#{VERSION}"}) + end + + def put_readme + cd(__dir__) + puts "Putting READMEs... " + Dir["npm/*"].each do |npm_dir| + cp(File.join(ROOT, "README.md"), File.join(npm_dir, "README.md"), verbose: true) + end + cp(File.join(ROOT, "README.md"), "npm-bundled/", verbose: true) + cp(File.join(ROOT, "README.md"), "npm-installer/", verbose: true) + puts "done" + end + + def put_binaries + cd(__dir__) + puts "Putting binaries to packages..." + { + "#{DIST}/no_self_update_linux_amd64_v1/lefthook" => "npm/lefthook-linux-x64/bin/lefthook", + "#{DIST}/no_self_update_linux_arm64/lefthook" => "npm/lefthook-linux-arm64/bin/lefthook", + "#{DIST}/no_self_update_freebsd_amd64_v1/lefthook" => "npm/lefthook-freebsd-x64/bin/lefthook", + "#{DIST}/no_self_update_freebsd_arm64/lefthook" => "npm/lefthook-freebsd-arm64/bin/lefthook", + "#{DIST}/no_self_update_windows_amd64_v1/lefthook.exe" => "npm/lefthook-windows-x64/bin/lefthook.exe", + "#{DIST}/no_self_update_windows_arm64/lefthook.exe" => "npm/lefthook-windows-arm64/bin/lefthook.exe", + "#{DIST}/no_self_update_darwin_amd64_v1/lefthook" => "npm/lefthook-darwin-x64/bin/lefthook", + "#{DIST}/no_self_update_darwin_arm64/lefthook" => "npm/lefthook-darwin-arm64/bin/lefthook", + }.each do |(source, dest)| + mkdir_p(File.dirname(dest)) + cp(source, dest, verbose: true) + end + + { + "#{DIST}/no_self_update_linux_amd64_v1/lefthook" => "npm-bundled/bin/lefthook-linux-x64/lefthook", + "#{DIST}/no_self_update_linux_arm64/lefthook" => "npm-bundled/bin/lefthook-linux-arm64/lefthook", + "#{DIST}/no_self_update_freebsd_amd64_v1/lefthook" => "npm-bundled/bin/lefthook-freebsd-x64/lefthook", + "#{DIST}/no_self_update_freebsd_arm64/lefthook" => "npm-bundled/bin/lefthook-freebsd-arm64/lefthook", + "#{DIST}/no_self_update_windows_amd64_v1/lefthook.exe" => "npm-bundled/bin/lefthook-windows-x64/lefthook.exe", + "#{DIST}/no_self_update_windows_arm64/lefthook.exe" => "npm-bundled/bin/lefthook-windows-arm64/lefthook.exe", + "#{DIST}/no_self_update_darwin_amd64_v1/lefthook" => "npm-bundled/bin/lefthook-darwin-x64/lefthook", + "#{DIST}/no_self_update_darwin_arm64/lefthook" => "npm-bundled/bin/lefthook-darwin-arm64/lefthook", + }.each do |(source, dest)| + mkdir_p(File.dirname(dest)) + cp(source, dest, verbose: true) + end + + { + "#{DIST}/no_self_update_linux_amd64_v1/lefthook" => "rubygems/libexec/lefthook-linux-x64/lefthook", + "#{DIST}/no_self_update_linux_arm64/lefthook" => "rubygems/libexec/lefthook-linux-arm64/lefthook", + "#{DIST}/no_self_update_freebsd_amd64_v1/lefthook" => "rubygems/libexec/lefthook-freebsd-x64/lefthook", + "#{DIST}/no_self_update_freebsd_arm64/lefthook" => "rubygems/libexec/lefthook-freebsd-arm64/lefthook", + "#{DIST}/no_self_update_windows_amd64_v1/lefthook.exe" => "rubygems/libexec/lefthook-windows-x64/lefthook.exe", + "#{DIST}/no_self_update_windows_arm64/lefthook.exe" => "rubygems/libexec/lefthook-windows-arm64/lefthook.exe", + "#{DIST}/no_self_update_darwin_amd64_v1/lefthook" => "rubygems/libexec/lefthook-darwin-x64/lefthook", + "#{DIST}/no_self_update_darwin_arm64/lefthook" => "rubygems/libexec/lefthook-darwin-arm64/lefthook", + }.each do |(source, dest)| + mkdir_p(File.dirname(dest)) + cp(source, dest, verbose: true) + end + + puts "done" + end + + def publish + puts "Publishing lefthook npm..." + cd(File.join(__dir__, "npm")) + Dir["lefthook*"].each do |package| + puts "publishing #{package}" + cd(File.join(__dir__, "npm", package)) + system("npm publish --access public", exception: true) + cd(File.join(__dir__, "npm")) + end + + puts "Publishing @evilmartians/lefthook npm..." + cd(File.join(__dir__, "npm-bundled")) + system("npm publish --access public", exception: true) + + puts "Publishing @evilmartians/lefthook-installer npm..." + cd(File.join(__dir__, "npm-installer")) + system("npm publish --access public", exception: true) + + puts "Publishing lefthook gem..." + cd(File.join(__dir__, "rubygems")) + system("rake build", exception: true) + system("gem push pkg/*.gem", exception: true) + + puts "done" + end + + def replace_in_file(filepath, regexp, value) + text = File.open(filepath, "r") do |f| + f.read + end + text.gsub!(regexp, value) + File.open(filepath, "w") do |f| + f.write(text) + end + end +end + +ARGV.each do |cmd| + Pack.public_send(cmd) +end diff --git a/packaging/rubygems/bin/lefthook b/packaging/rubygems/bin/lefthook index 7638c420..77d6f808 100755 --- a/packaging/rubygems/bin/lefthook +++ b/packaging/rubygems/bin/lefthook @@ -7,8 +7,8 @@ arch = case platform.cpu.sub(/\Auniversal\./, '') when /\Aarm64/ then "arm64" # Apple reports arm64e on M1 macs when /aarch64/ then "arm64" - when "x86_64" then "amd64" - when "x64" then "amd64" # Windows with MINGW64 reports RUBY_PLATFORM as "x64-mingw32" + when "x86_64" then "x64" + when "x64" then "x64" # Windows with MINGW64 reports RUBY_PLATFORM as "x64-mingw32" else raise "Unknown architecture: #{platform.cpu}" end @@ -18,14 +18,12 @@ os = when "darwin" then "darwin" # MacOS when "windows" then "windows" when "mingw32" then "windows" # Windows with MINGW64 reports RUBY_PLATFORM as "x64-mingw32" - when "mingw" then "windows" + when "mingw" then "windows" when "freebsd" then "freebsd" else raise "Unknown OS: #{platform.os}" end -suffix = arch == "amd64" ? "_v1" : "" # GOAMD64 - -binary = "lefthook_#{os}_#{arch}#{suffix}/lefthook" +binary = "lefthook-#{os}-#{arch}/lefthook" binary = "#{binary}.exe" if os == "windows" args = $*.map { |x| x.include?(' ') ? "'" + x + "'" : x } diff --git a/packaging/rubygems/lefthook.gemspec b/packaging/rubygems/lefthook.gemspec index bc2def0f..27a96176 100644 --- a/packaging/rubygems/lefthook.gemspec +++ b/packaging/rubygems/lefthook.gemspec @@ -1,6 +1,6 @@ Gem::Specification.new do |spec| spec.name = "lefthook" - spec.version = "1.6.14" + spec.version = "1.7.9" spec.authors = ["A.A.Abroskin", "Evil Martians"] spec.email = ["lefthook@evilmartians.com"] diff --git a/testdata/exclude.txt b/testdata/exclude.txt new file mode 100644 index 00000000..8213e8b4 --- /dev/null +++ b/testdata/exclude.txt @@ -0,0 +1,51 @@ +exec git init +exec lefthook install +exec git config user.email "you@example.com" +exec git config user.name "Your Name" +exec git add -A +exec lefthook run -f all +stdout 'a.txt b.txt dir/a.txt dir/b.txt lefthook.yml' +exec lefthook run -f regexp +stdout 'dir/a.txt dir/b.txt lefthook.yml' +exec lefthook run -f array +stdout 'dir/a.txt dir/b.txt' + +-- lefthook.yml -- +skip_output: + - skips + - meta + - summary + - execution_info +all: + commands: + echo: + run: echo {staged_files} + +regexp: + commands: + echo: + run: echo {staged_files} + exclude: '^(a.txt|b.txt)' + +array: + commands: + echo: + run: echo {staged_files} + exclude: + - a.txt + - b.txt + - '*.yml' + +-- a.txt -- +a + +-- b.txt -- +b + +-- dir/a.txt -- +dir-a + +-- dir/b.txt -- +dir-b + + diff --git a/testdata/many_extends_levels.txt b/testdata/many_extends_levels.txt new file mode 100644 index 00000000..1edc6c58 --- /dev/null +++ b/testdata/many_extends_levels.txt @@ -0,0 +1,80 @@ +[windows] skip + +exec git init +exec lefthook dump +cmp stdout dump.yml +! stderr . + +-- lefthook.yml -- +extends: + - extends/e1.yml + +pre-commit: + commands: + echo: + run: echo 0 + +-- extends/e1.yml -- +extends: + - extends/e2.yml + +pre-commit: + commands: + echo: + run: echo 1 + skip: true + +e1: + commands: + echo: + run: e1 + +-- extends/e2.yml -- +extends: + - extends/e3.yml + +pre-commit: + commands: + echo: + run: echo 2 + tags: ["backend"] + +e2: + commands: + echo: + run: e2 + +-- extends/e3.yml -- +pre-commit: + commands: + echo: + glob: 3 + +e3: + commands: + echo: + run: e3 + +-- dump.yml -- +e1: + commands: + echo: + run: e1 +e2: + commands: + echo: + run: e2 +e3: + commands: + echo: + run: e3 +extends: + - extends/e3.yml +pre-commit: + commands: + echo: + run: echo 2 + skip: true + tags: + - backend + glob: "3"