├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── codeql.yml │ ├── gh-pages.yml │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .lefthook.yml ├── .typos.toml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── book.toml ├── cliff.toml ├── cmd ├── add-doc.txt ├── add.go ├── commands.go ├── commands_no_self_update.go ├── dump.go ├── install.go ├── lefthook.go ├── root.go ├── run.go ├── self_update.go ├── uninstall.go ├── validate.go └── version.go ├── docs ├── configuration.md ├── install.md ├── mdbook │ ├── SUMMARY.md │ ├── configuration │ │ ├── Commands.md │ │ ├── Hook.md │ │ ├── README.md │ │ ├── Scripts.md │ │ ├── assert_lefthook_installed.md │ │ ├── colors.md │ │ ├── configs.md │ │ ├── env.md │ │ ├── exclude.md │ │ ├── exclude_tags.md │ │ ├── extends.md │ │ ├── fail_text.md │ │ ├── file_types.md │ │ ├── files-global.md │ │ ├── files.md │ │ ├── follow.md │ │ ├── git_url.md │ │ ├── glob.md │ │ ├── group.md │ │ ├── interactive.md │ │ ├── jobs.md │ │ ├── lefthook.md │ │ ├── min_version.md │ │ ├── name.md │ │ ├── no_tty.md │ │ ├── only.md │ │ ├── output.md │ │ ├── parallel.md │ │ ├── piped.md │ │ ├── priority.md │ │ ├── rc.md │ │ ├── ref.md │ │ ├── refetch.md │ │ ├── refetch_frequency.md │ │ ├── remotes.md │ │ ├── root.md │ │ ├── run.md │ │ ├── runner.md │ │ ├── script.md │ │ ├── skip.md │ │ ├── skip_lfs.md │ │ ├── skip_output.md │ │ ├── source_dir.md │ │ ├── source_dir_local.md │ │ ├── stage_fixed.md │ │ ├── tags.md │ │ ├── templates.md │ │ └── use_stdin.md │ ├── contributors.md │ ├── examples │ │ ├── README.md │ │ ├── commitlint.md │ │ ├── filters.md │ │ ├── lefthook-local.md │ │ ├── remotes.md │ │ ├── skip.md │ │ └── stage_fixed.md │ ├── favicon.svg │ ├── installation │ │ ├── README.md │ │ ├── alpine.md │ │ ├── arch.md │ │ ├── deb.md │ │ ├── go.md │ │ ├── homebrew.md │ │ ├── manual.md │ │ ├── mise.md │ │ ├── node.md │ │ ├── python.md │ │ ├── rpm.md │ │ ├── ruby.md │ │ ├── scoop.md │ │ ├── snap.md │ │ ├── swift.md │ │ └── winget.md │ ├── intro.md │ ├── misc │ │ └── contributors.md │ └── usage │ │ ├── README.md │ │ ├── commands.md │ │ ├── env.md │ │ └── tips.md └── usage.md ├── examples ├── commitlint │ ├── README.md │ ├── commitlint.config.js │ └── lefthook.yml ├── complete │ └── lefthook.yml ├── remote │ └── ping.yml ├── verbose │ └── lefthook.yml └── with_scripts │ └── lefthook.yml ├── go.mod ├── go.sum ├── integrity_test.go ├── internal ├── config │ ├── available_hooks.go │ ├── command.go │ ├── command_executor.go │ ├── config.go │ ├── files.go │ ├── hook.go │ ├── job.go │ ├── load.go │ ├── load_test.go │ ├── remote.go │ ├── script.go │ ├── skip_checker.go │ └── skip_checker_test.go ├── gen │ └── jsonschema.go ├── git │ ├── command_executor.go │ ├── lfs.go │ ├── remote.go │ ├── repository.go │ ├── repository_test.go │ └── state.go ├── lefthook │ ├── add.go │ ├── add_test.go │ ├── dump.go │ ├── install.go │ ├── install_test.go │ ├── lefthook.go │ ├── run.go │ ├── run_test.go │ ├── runner │ │ ├── cached_reader.go │ │ ├── cached_reader_test.go │ │ ├── exec │ │ │ ├── execute_unix.go │ │ │ ├── execute_windows.go │ │ │ └── executor.go │ │ ├── filters │ │ │ ├── detect_text.go │ │ │ ├── detect_text_test.go │ │ │ ├── filters.go │ │ │ └── filters_test.go │ │ ├── jobs │ │ │ ├── build_command.go │ │ │ ├── build_command_test.go │ │ │ ├── build_script.go │ │ │ ├── jobs.go │ │ │ └── skip_error.go │ │ ├── result.go │ │ ├── run_jobs.go │ │ ├── runner.go │ │ └── runner_test.go │ ├── uninstall.go │ ├── uninstall_test.go │ └── validate.go ├── log │ ├── builder.go │ ├── log.go │ ├── settings.go │ └── settings_test.go ├── system │ ├── limits.go │ ├── null_reader.go │ ├── null_reader_test.go │ └── system.go ├── templates │ ├── config.tmpl │ ├── hook.tmpl │ └── templates.go ├── updater │ ├── updater.go │ └── updater_test.go └── version │ └── version.go ├── logo.svg ├── logo_sign.svg ├── main.go ├── packaging ├── aur │ ├── lefthook-bin │ │ └── PKGBUILD │ └── lefthook │ │ └── PKGBUILD ├── npm-bundled │ ├── bin │ │ └── index.js │ ├── get-exe.js │ ├── package.json │ └── postinstall.js ├── npm-installer │ ├── bin │ │ └── index.js │ ├── install.js │ └── package.json ├── npm │ ├── lefthook-darwin-arm64 │ │ └── package.json │ ├── lefthook-darwin-x64 │ │ └── package.json │ ├── lefthook-freebsd-arm64 │ │ └── package.json │ ├── lefthook-freebsd-x64 │ │ └── package.json │ ├── lefthook-linux-arm64 │ │ └── package.json │ ├── lefthook-linux-x64 │ │ └── package.json │ ├── lefthook-openbsd-arm64 │ │ └── package.json │ ├── lefthook-openbsd-x64 │ │ └── package.json │ ├── lefthook-windows-arm64 │ │ └── package.json │ ├── lefthook-windows-x64 │ │ └── package.json │ └── lefthook │ │ ├── bin │ │ └── index.js │ │ ├── get-exe.js │ │ ├── package.json │ │ └── postinstall.js ├── pack.rb ├── pypi │ ├── LICENSE │ ├── README.md │ ├── lefthook │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── bin │ │ │ └── .keep │ │ └── main.py │ └── setup.py └── rubygems │ ├── Gemfile │ ├── README.md │ ├── Rakefile │ ├── bin │ └── lefthook │ ├── lefthook.gemspec │ ├── lib │ └── lefthook.rb │ └── libexec │ └── .keep ├── schema.json ├── tea.yaml └── testdata ├── add.txt ├── cli_run_only.txt ├── dump.txt ├── exclude.txt ├── fail_text.txt ├── files_override.txt ├── filter_by_file_type.txt ├── hide_unstaged.txt ├── install.txt ├── job_fail_text.txt ├── job_filter_by_file_type.txt ├── job_merging.txt ├── job_stage_fixed.txt ├── lefthook_option.txt ├── many_extends_levels.txt ├── min_version.txt ├── pre-commit_issue_919.txt ├── remote.txt ├── remotes.txt ├── run_interrupt.txt ├── run_json.txt ├── run_non_existing.txt ├── run_script.txt ├── run_toml.txt ├── run_yml.txt ├── sh_syntax_in_files.txt ├── skip_merge_commit.txt ├── skip_run.txt ├── skip_run_windows.txt ├── stage_fixed.txt ├── stage_fixed_505.txt ├── templates.txt ├── uninstall.txt └── version.txt /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Report a bug 3 | about: Found something broken? Let us know! If it's not yet reproducible, please `Ask a question` instead. 4 | labels: 'bug' 5 | --- 6 | 7 | ### :wrench: Summary 8 | 9 | 10 | 11 | ### Lefthook version 12 | 13 | 14 | 15 | ### Steps to reproduce 16 | 17 | 18 | 19 | ### Expected results 20 | 21 | 22 | 23 | ### Actual results 24 | 25 | 26 | 27 | ### Possible Solution 28 | 29 | 30 | 31 | ### Logs / Screenshots 32 | 33 | 34 | 35 | ```bash 36 | LEFTHOOK_VERBOSE=true git ... 37 | ``` 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | blank_issues_enabled: false 3 | 4 | contact_links: 5 | - name: 💡 Discuss an idea 6 | url: https://github.com/evilmartians/lefthook/discussions/new?category=ideas 7 | about: Suggest a feature or an improvement. 8 | 9 | - name: ❔ Ask a question 10 | url: https://github.com/evilmartians/lefthook/discussions/new 11 | about: Ask questions and discuss with other `lefthook` users or maintainers. 12 | 13 | - name: 🙏 Request help 14 | url: https://github.com/evilmartians/lefthook/discussions/new 15 | about: Ask the `lefthook` community for help. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ⭐ Feature request 3 | about: Want something to be implemented in `lefthook`? Create a feature request! If you are not sure, or just have an idea, please `Discuss an idea` instead. 4 | labels: 'feature request' 5 | --- 6 | 7 | ### :zap: Summary 8 | 9 | 10 | 11 | ### Value 12 | 13 | 14 | 15 | ### Behavior and configuration changes 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Closes # (issue) 2 | 3 | 4 | 5 | #### :zap: Summary 6 | 7 | 8 | 9 | #### :ballot_box_with_check: Checklist 10 | 11 | - [ ] Check locally 12 | - [ ] Add tests 13 | - [ ] Add documentation 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | target-branch: "dependencies" 7 | schedule: 8 | interval: "weekly" 9 | day: "monday" 10 | time: "06:00" # 6:00 UTC 11 | commit-message: 12 | prefix: "deps" 13 | assignees: 14 | - "mrexox" 15 | reviewers: 16 | - "mrexox" 17 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "master" ] 9 | schedule: 10 | # 6:00 UTC on Monday 11 | - cron: '0 6 * * 1' 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | permissions: 18 | actions: read 19 | contents: read 20 | security-events: write 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | language: [ 'go' ] 26 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 27 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: ${{ matrix.language }} 38 | # If you wish to specify custom queries, you can do so here or in a config file. 39 | # By default, queries listed here will override any specified in a config file. 40 | # Prefix the list here with "+" to use these queries and those in the config file. 41 | 42 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 43 | # queries: security-extended,security-and-quality 44 | 45 | 46 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 47 | # If this step fails, then you should remove it and run the build manually (see below) 48 | - name: Autobuild 49 | uses: github/codeql-action/autobuild@v3 50 | 51 | # ℹ️ Command-line programs to run using the OS shell. 52 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 53 | 54 | # If the Autobuild fails above, remove it and uncomment the following three lines. 55 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 56 | 57 | # - run: | 58 | # echo "Run, Build Application using script" 59 | # ./location_of_script_within_repo/buildscript.sh 60 | 61 | - name: Perform CodeQL Analysis 62 | uses: github/codeql-action/analyze@v3 63 | with: 64 | category: "/language:${{matrix.language}}" 65 | 66 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | gh-pages: 11 | runs-on: ubuntu-latest 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Setup mdBook 18 | uses: peaceiris/actions-mdbook@v2 19 | with: 20 | mdbook-version: '0.4.47' 21 | 22 | - run: mdbook build 23 | 24 | - name: Deploy 25 | uses: peaceiris/actions-gh-pages@v3 26 | if: ${{ github.ref == 'refs/heads/master' }} 27 | with: 28 | github_token: ${{ secrets.GITHUB_TOKEN }} 29 | publish_dir: ./book 30 | cname: lefthook.dev 31 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | 7 | name: Lint 8 | jobs: 9 | golangci: 10 | name: golangci-lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Install Go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version-file: go.mod 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@v8 20 | with: 21 | version: v2.1.6 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .idea/ 3 | /lefthook 4 | /lefthook-local.yml 5 | 6 | tmp/ 7 | dist/ 8 | book/ 9 | 10 | # Packages 11 | packaging/pypi/lefthook/__pycache__/ 12 | packaging/pypi/lefthook/bin/ 13 | packaging/pypi/lefthook.egg-info/ 14 | packaging/pypi/build/ 15 | packaging/rubygems/pkg/ 16 | packaging/rubygems/libexec/ 17 | packaging/npm-bundled/bin/ 18 | packaging/npm-*/README.md 19 | packaging/npm/*/bin/ 20 | packaging/npm/*/README.md 21 | packaging/npm/*/schema.json 22 | packaging/npm-bundled/schema.json 23 | packaging/npm-installer/schema.json 24 | !packaging/npm/*/package.json 25 | !packaging/npm/lefthook/bin/index.js 26 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | default: none 5 | enable: 6 | - asasalint 7 | - asciicheck 8 | - bidichk 9 | - bodyclose 10 | - containedctx 11 | - contextcheck 12 | - copyloopvar 13 | - dogsled 14 | - dupl 15 | - dupword 16 | - durationcheck 17 | - errcheck 18 | - errchkjson 19 | - errname 20 | - errorlint 21 | - exhaustive 22 | - forbidigo 23 | - gochecknoinits 24 | - goconst 25 | - gocritic 26 | - gocyclo 27 | - godot 28 | - godox 29 | - goheader 30 | - goprintffuncname 31 | - govet 32 | - ineffassign 33 | - intrange 34 | - makezero 35 | - mirror 36 | - misspell 37 | - mnd 38 | - nestif 39 | - noctx 40 | - nolintlint 41 | - perfsprint 42 | - prealloc 43 | - predeclared 44 | - reassign 45 | - revive 46 | - staticcheck 47 | - tagalign 48 | - usetesting 49 | - unconvert 50 | - unparam 51 | - unused 52 | - usestdlibvars 53 | - whitespace 54 | settings: 55 | gocritic: 56 | disabled-checks: 57 | - hugeParam 58 | enabled-tags: 59 | - performance 60 | govet: 61 | enable: 62 | - shadow 63 | misspell: 64 | locale: US 65 | perfsprint: 66 | strconcat: false 67 | revive: 68 | rules: 69 | - name: unused-parameter 70 | disabled: true 71 | unused: 72 | field-writes-are-uses: false 73 | post-statements-are-reads: true 74 | exported-fields-are-used: false 75 | local-variables-are-used: false 76 | generated-is-used: false 77 | 78 | formatters: 79 | enable: 80 | - gci 81 | - gofumpt 82 | - goimports 83 | settings: 84 | gci: 85 | sections: 86 | - standard 87 | - default 88 | - prefix(github.com/evilmartians/lefthook) 89 | -------------------------------------------------------------------------------- /.lefthook.yml: -------------------------------------------------------------------------------- 1 | assert_lefthook_installed: true 2 | skip_lfs: true 3 | 4 | pre-commit: 5 | parallel: true 6 | jobs: 7 | - name: lint & test 8 | glob: "*.go" 9 | group: 10 | jobs: 11 | - run: make lint 12 | stage_fixed: true 13 | 14 | - run: make test 15 | 16 | - name: check links 17 | run: lychee --max-concurrency 3 {staged_files} 18 | glob: '*.md' 19 | exclude: 20 | - CHANGELOG.md 21 | 22 | - name: fix typos 23 | run: typos --write-changes {staged_files} 24 | stage_fixed: true 25 | 26 | - name: update JSON schema 27 | run: go generate internal/gen/jsonschema.go > schema.json && git add schema.json 28 | glob: 29 | - 'internal/config/command.go' 30 | - 'internal/config/config.go' 31 | - 'internal/config/hook.go' 32 | - 'internal/config/job.go' 33 | - 'internal/config/remote.go' 34 | - 'internal/config/script.go' 35 | -------------------------------------------------------------------------------- /.typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-identifiers] 2 | "PnP" = "PnP" 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thanks for taking the time to contribute! Feel free to make Pull Request with your changes. 4 | 5 | # Requirements 6 | 7 | Go >= 1.24.0 8 | 9 | # Process 10 | 11 | 1. Fork repo 12 | 2. git clone 13 | 3. Make changes 14 | 4. Push your changes in 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 Arkweid 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COMMIT_HASH = $(shell git rev-parse HEAD) 2 | 3 | build: 4 | go build -ldflags "-s -w -X github.com/evilmartians/lefthook/internal/version.commit=$(COMMIT_HASH)" -o lefthook 5 | 6 | build-with-coverage: 7 | go build -cover -ldflags "-s -w -X github.com/evilmartians/lefthook/internal/version.commit=$(COMMIT_HASH)" -o lefthook 8 | 9 | jsonschema: 10 | go generate internal/gen/jsonschema.go > schema.json 11 | 12 | install: build 13 | ifeq ($(shell go env GOOS),windows) 14 | copy lefthook $(shell go env GOPATH)\bin\lefthook.exe 15 | else 16 | cp lefthook $$(go env GOPATH)/bin 17 | endif 18 | 19 | test: 20 | go test -cpu 24 -race -count=1 -timeout=30s ./... 21 | 22 | test-integrity: install 23 | go test -cpu 24 -race -count=1 -timeout=30s -tags=integrity integrity_test.go 24 | 25 | bench: 26 | go test -cpu 24 -race -run=Bench -bench=. ./... 27 | 28 | bin/golangci-lint: 29 | @test -x $$(go env GOPATH)/bin/golangci-lint || \ 30 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin v2.1.6 31 | 32 | lint: bin/golangci-lint 33 | $$(go env GOPATH)/bin/golangci-lint run --fix 34 | 35 | .ONESHELL: 36 | version: 37 | @read -p "New version: " version 38 | sed -i "s/const version = .*/const version = \"$$version\"/" internal/version/version.go 39 | sed -i "s/VERSION = .*/VERSION = \"$$version\"/" packaging/pack.rb 40 | sed -i "s/lefthook-plugin.git\", exact: \".*\"/lefthook-plugin.git\", exact: \"$$version\"/" docs/install.md 41 | sed -i "s/lefthook-plugin.git\", exact: \".*\"/lefthook-plugin.git\", exact: \"$$version\"/" docs/mdbook/installation/swift.md 42 | ruby packaging/pack.rb clean set_version 43 | git add internal/version/version.go packaging/* docs/ 44 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Latest major version of Lefthook is being supported with security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.x | :white_check_mark: | 10 | | 0.x | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | If you have found a security issue in Lefthook, please **do not** create a new issue in the GitHub repository. Instead, please send an email to [lefthook@evilmartians.com](mailto:lefthook@evilmartians.com?subject=Lefthook%3A%20security%20issue) describing what the problem is and how to reproduce it. We will get in touch with you! 15 | 16 | Please note that Lefthook, as a CLI tool, executes arbitrary commands and scripts from its configuration file by design. This is intended behavior. Feel free to join the discussion on [issue #229](https://github.com/evilmartians/lefthook/issues/229). 17 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Evil Martians"] 3 | language = "en" 4 | multilingual = false 5 | src = "docs/mdbook" 6 | title = "Lefthook Documentation" 7 | 8 | [output.html] 9 | no-section-label = true 10 | git-repository-url = "https://github.com/evilmartians/lefthook" 11 | 12 | [output.html.fold] 13 | enable = true 14 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # https://git-cliff.org/docs/configuration 2 | 3 | [changelog] 4 | header = "# Change log\n\n" 5 | body = """ 6 | {% if version %}\ 7 | ## {{ version | trim_start_matches(pat="v") }} ({{ timestamp | date(format="%Y-%m-%d") }}) 8 | {% else %}\ 9 | ## (unreleased) 10 | {% endif %} 11 | {% for commit in commits %}\ 12 | - {{ commit.group }}: {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }} by {% if commit.remote.username %}[@{{commit.remote.username}}](https://github.com/{{commit.remote.username}}) {% else %} {{ commit.author.name }}{% endif %} 13 | {% endfor %}\n 14 | """ 15 | trim = true 16 | 17 | [git] 18 | # parse the commits based on https://www.conventionalcommits.org 19 | conventional_commits = true 20 | # filter out the commits that are not conventional 21 | filter_unconventional = true 22 | # process each line of a commit as an individual commit 23 | split_commits = false 24 | # regex for preprocessing the commit messages 25 | commit_preprocessors = [ 26 | { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/evilmartians/lefthook/pull/${2}))"}, # replace issue numbers 27 | ] 28 | # regex for parsing and grouping commits 29 | commit_parsers = [ 30 | { message = "^feat", group = "feat" }, 31 | { message = "^fix", group = "fix" }, 32 | { message = "^docs", group = "docs" }, 33 | { message = "^perf", group = "perf" }, 34 | { message = "^refactor", group = "refactor" }, 35 | { message = "^ci", group = "ci" }, 36 | { message = "^test", group = "test" }, 37 | { message = "^chore\\(release\\): prepare for", skip = true }, 38 | { message = "^chore", group = "chore" }, 39 | { body = ".*security", group = "security" }, 40 | ] 41 | # protect breaking changes from being skipped due to matching a skipping commit_parser 42 | protect_breaking_commits = false 43 | # filter out the commits that are not matched by commit parsers 44 | filter_commits = false 45 | # glob pattern for matching git tags 46 | tag_pattern = "v[0-9]*" 47 | # regex for ignoring tags 48 | ignore_tags = "" 49 | # sort the tags topologically 50 | topo_order = false 51 | # sort the commits inside sections by oldest/newest order 52 | sort_commits = "newest" 53 | # limit the number of commits included in the changelog. 54 | # limit_commits = 42 55 | -------------------------------------------------------------------------------- /cmd/add-doc.txt: -------------------------------------------------------------------------------- 1 | This command will try to build the following structure in repository: 2 | ├───.git 3 | │ └───hooks 4 | │ └───pre-commit // this executable will be added. Existing file with 5 | │ // same name will be renamed to pre-commit.old 6 | (lefthook adds these dirs if you run the command with the -d option) 7 | │ 8 | ├───.lefthook // directory for project level hooks 9 | │ └───pre-commit // directory with hook executables 10 | └───.lefthook-local // directory for personal hooks; add it in .gitignore 11 | └───pre-commit 12 | -------------------------------------------------------------------------------- /cmd/add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | _ "embed" 5 | "maps" 6 | "slices" 7 | 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/evilmartians/lefthook/internal/config" 11 | "github.com/evilmartians/lefthook/internal/lefthook" 12 | ) 13 | 14 | //go:embed add-doc.txt 15 | var addDoc string 16 | 17 | type add struct{} 18 | 19 | func (add) New(opts *lefthook.Options) *cobra.Command { 20 | args := lefthook.AddArgs{} 21 | 22 | addHookCompletions := func(cmd *cobra.Command, args []string, toComplete string) (ret []string, compDir cobra.ShellCompDirective) { 23 | compDir = cobra.ShellCompDirectiveNoFileComp 24 | if len(args) != 0 { 25 | return 26 | } 27 | ret = slices.Sorted(maps.Keys(config.AvailableHooks)) 28 | return 29 | } 30 | 31 | addCmd := cobra.Command{ 32 | Use: "add hook-name", 33 | Short: "This command adds a hook directory to a repository", 34 | Long: addDoc, 35 | Example: "lefthook add pre-commit", 36 | ValidArgsFunction: addHookCompletions, 37 | Args: cobra.ExactArgs(1), 38 | RunE: func(_cmd *cobra.Command, hooks []string) error { 39 | args.Hook = hooks[0] 40 | return lefthook.Add(opts, &args) 41 | }, 42 | } 43 | 44 | addCmd.Flags().BoolVarP( 45 | &args.CreateDirs, "dirs", "d", false, "create directory for scripts", 46 | ) 47 | addCmd.Flags().BoolVarP( 48 | &args.Force, "force", "f", false, "overwrite .old hooks", 49 | ) 50 | 51 | return &addCmd 52 | } 53 | -------------------------------------------------------------------------------- /cmd/commands.go: -------------------------------------------------------------------------------- 1 | //go:build !no_self_update && !jsonschema 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/evilmartians/lefthook/internal/lefthook" 9 | ) 10 | 11 | type command interface { 12 | New(*lefthook.Options) *cobra.Command 13 | } 14 | 15 | var commands = [...]command{ 16 | version{}, 17 | add{}, 18 | install{}, 19 | uninstall{}, 20 | run{}, 21 | dump{}, 22 | selfUpdate{}, 23 | validate{}, 24 | } 25 | -------------------------------------------------------------------------------- /cmd/commands_no_self_update.go: -------------------------------------------------------------------------------- 1 | //go:build no_self_update && !jsonschema 2 | 3 | package cmd 4 | 5 | import ( 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/evilmartians/lefthook/internal/lefthook" 9 | ) 10 | 11 | type command interface { 12 | New(*lefthook.Options) *cobra.Command 13 | } 14 | 15 | var commands = [...]command{ 16 | version{}, 17 | add{}, 18 | install{}, 19 | uninstall{}, 20 | run{}, 21 | dump{}, 22 | validate{}, 23 | } 24 | -------------------------------------------------------------------------------- /cmd/dump.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/evilmartians/lefthook/internal/lefthook" 7 | "github.com/evilmartians/lefthook/internal/log" 8 | ) 9 | 10 | type dump struct{} 11 | 12 | func (dump) New(opts *lefthook.Options) *cobra.Command { 13 | dumpArgs := lefthook.DumpArgs{} 14 | dumpCmd := cobra.Command{ 15 | Use: "dump", 16 | Short: "Prints config merged from all extensions (in YAML format by default)", 17 | Example: "lefthook dump", 18 | ValidArgsFunction: cobra.NoFileCompletions, 19 | Args: cobra.NoArgs, 20 | Run: func(cmd *cobra.Command, args []string) { 21 | lefthook.Dump(opts, dumpArgs) 22 | }, 23 | } 24 | 25 | dumpCmd.Flags().StringVarP( 26 | &dumpArgs.Format, "format", "f", "yaml", "'yaml', 'toml', or 'json'", 27 | ) 28 | 29 | dumpCmd.Flags().BoolVarP( 30 | &dumpArgs.JSON, "json", "j", false, 31 | "dump in JSON format", 32 | ) 33 | 34 | dumpCmd.Flags().BoolVarP( 35 | &dumpArgs.TOML, "toml", "t", false, 36 | "dump in TOML format", 37 | ) 38 | 39 | err := dumpCmd.Flags().MarkDeprecated("json", "use --format=json") 40 | if err != nil { 41 | log.Warn("Unexpected error:", err) 42 | } 43 | 44 | err = dumpCmd.Flags().MarkDeprecated("toml", "use --format=toml") 45 | if err != nil { 46 | log.Warn("Unexpected error:", err) 47 | } 48 | 49 | return &dumpCmd 50 | } 51 | -------------------------------------------------------------------------------- /cmd/install.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/evilmartians/lefthook/internal/lefthook" 7 | "github.com/evilmartians/lefthook/internal/log" 8 | ) 9 | 10 | type install struct{} 11 | 12 | func (install) New(opts *lefthook.Options) *cobra.Command { 13 | var a, force bool 14 | 15 | installCmd := cobra.Command{ 16 | Use: "install", 17 | Short: "Write a basic configuration file in your project repository, or initialize the existing configuration", 18 | ValidArgsFunction: cobra.NoFileCompletions, 19 | Args: cobra.NoArgs, 20 | RunE: func(cmd *cobra.Command, _args []string) error { 21 | return lefthook.Install(opts, force) 22 | }, 23 | } 24 | 25 | // To be dropped in next releases. 26 | installCmd.Flags().BoolVarP( 27 | &force, "force", "f", false, 28 | "overwrite .old hooks", 29 | ) 30 | installCmd.Flags().BoolVarP( 31 | &a, "aggressive", "a", false, 32 | "use --force flag instead", 33 | ) 34 | 35 | err := installCmd.Flags().MarkDeprecated("aggressive", "use --force flag instead") 36 | if err != nil { 37 | log.Warn("Unexpected error:", err) 38 | } 39 | 40 | return &installCmd 41 | } 42 | -------------------------------------------------------------------------------- /cmd/lefthook.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "github.com/evilmartians/lefthook/internal/log" 4 | 5 | func Lefthook() int { 6 | rootCmd := newRootCmd() 7 | 8 | if err := rootCmd.Execute(); err != nil { 9 | if err.Error() != "" { 10 | log.Errorf("Error: %s", err) 11 | } 12 | return 1 13 | } 14 | 15 | return 0 16 | } 17 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/MakeNowJust/heredoc" 5 | "github.com/spf13/afero" 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/evilmartians/lefthook/internal/lefthook" 9 | "github.com/evilmartians/lefthook/internal/log" 10 | ) 11 | 12 | func newRootCmd() *cobra.Command { 13 | options := lefthook.Options{ 14 | Fs: afero.NewOsFs(), 15 | } 16 | 17 | rootCmd := &cobra.Command{ 18 | Use: "lefthook", 19 | Short: "CLI tool to manage Git hooks", 20 | Long: heredoc.Doc(` 21 | After installation go to your project directory 22 | and execute the following command: 23 | lefthook install 24 | `), 25 | SilenceUsage: true, 26 | SilenceErrors: true, 27 | } 28 | 29 | rootCmd.PersistentFlags().BoolVarP( 30 | &options.Verbose, "verbose", "v", false, "verbose output", 31 | ) 32 | 33 | rootCmd.PersistentFlags().StringVar( 34 | &options.Colors, "colors", "auto", "'auto', 'on', or 'off'", 35 | ) 36 | 37 | rootCmd.PersistentFlags().BoolVar( 38 | &options.NoColors, "no-colors", false, "disable colored output", 39 | ) 40 | 41 | // To be dropped in next releases. 42 | rootCmd.Flags().BoolVarP( 43 | &options.Force, "force", "f", false, 44 | "use command-specific --force option", 45 | ) 46 | rootCmd.Flags().BoolVarP( 47 | &options.Aggressive, "aggressive", "a", false, 48 | "use --force flag instead", 49 | ) 50 | err := rootCmd.PersistentFlags().MarkDeprecated("no-colors", "use --colors") 51 | if err != nil { 52 | log.Warn("Unexpected error:", err) 53 | } 54 | err = rootCmd.Flags().MarkDeprecated("aggressive", "use command-specific --force option") 55 | if err != nil { 56 | log.Warn("Unexpected error:", err) 57 | } 58 | err = rootCmd.Flags().MarkDeprecated("force", "use command-specific --force option") 59 | if err != nil { 60 | log.Warn("Unexpected error:", err) 61 | } 62 | 63 | for _, subcommand := range commands { 64 | rootCmd.AddCommand(subcommand.New(&options)) 65 | } 66 | 67 | return rootCmd 68 | } 69 | -------------------------------------------------------------------------------- /cmd/self_update.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/signal" 8 | "syscall" 9 | 10 | "github.com/spf13/cobra" 11 | 12 | "github.com/evilmartians/lefthook/internal/lefthook" 13 | "github.com/evilmartians/lefthook/internal/log" 14 | "github.com/evilmartians/lefthook/internal/updater" 15 | ) 16 | 17 | type selfUpdate struct{} 18 | 19 | func (selfUpdate) New(opts *lefthook.Options) *cobra.Command { 20 | var yes bool 21 | upgradeCmd := cobra.Command{ 22 | Use: "self-update", 23 | Short: "Update lefthook executable", 24 | Example: "lefthook self-update", 25 | ValidArgsFunction: cobra.NoFileCompletions, 26 | Args: cobra.NoArgs, 27 | RunE: func(_cmd *cobra.Command, _args []string) error { 28 | return update(opts, yes) 29 | }, 30 | } 31 | 32 | upgradeCmd.Flags().BoolVarP(&yes, "yes", "y", false, "no prompt") 33 | upgradeCmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "force upgrade") 34 | upgradeCmd.Flags().BoolVarP(&opts.Verbose, "verbose", "v", false, "show verbose logs") 35 | 36 | return &upgradeCmd 37 | } 38 | 39 | func update(opts *lefthook.Options, yes bool) error { 40 | if os.Getenv(lefthook.EnvVerbose) == "1" || os.Getenv(lefthook.EnvVerbose) == "true" { 41 | opts.Verbose = true 42 | } 43 | if opts.Verbose { 44 | log.SetLevel(log.DebugLevel) 45 | log.Debug("Verbose mode enabled") 46 | } 47 | 48 | ctx, cancel := context.WithCancel(context.Background()) 49 | defer cancel() 50 | 51 | // Handle interrupts 52 | signalChan := make(chan os.Signal, 1) 53 | signal.Notify( 54 | signalChan, 55 | syscall.SIGINT, 56 | syscall.SIGTERM, 57 | ) 58 | go func() { 59 | <-signalChan 60 | cancel() 61 | }() 62 | 63 | exePath, err := os.Executable() 64 | if err != nil { 65 | return fmt.Errorf("failed to determine the binary path: %w", err) 66 | } 67 | 68 | return updater.New().SelfUpdate(ctx, updater.Options{ 69 | Yes: yes, 70 | Force: opts.Force, 71 | ExePath: exePath, 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /cmd/uninstall.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/evilmartians/lefthook/internal/lefthook" 7 | ) 8 | 9 | type uninstall struct{} 10 | 11 | func (uninstall) New(opts *lefthook.Options) *cobra.Command { 12 | args := lefthook.UninstallArgs{} 13 | 14 | uninstallCmd := cobra.Command{ 15 | Use: "uninstall", 16 | Short: "Revert install command", 17 | ValidArgsFunction: cobra.NoFileCompletions, 18 | Args: cobra.NoArgs, 19 | RunE: func(cmd *cobra.Command, _args []string) error { 20 | return lefthook.Uninstall(opts, &args) 21 | }, 22 | } 23 | 24 | uninstallCmd.Flags().BoolVarP( 25 | &args.Force, "aggressive", "a", false, 26 | "DEPRECATED: will behave like -f/--force option", 27 | ) 28 | 29 | uninstallCmd.Flags().BoolVarP( 30 | &args.Force, "force", "f", false, 31 | "remove all git hooks even not lefthook-related", 32 | ) 33 | 34 | uninstallCmd.Flags().BoolVarP( 35 | &args.RemoveConfig, "remove-configs", "c", false, 36 | "remove lefthook main and secondary config files", 37 | ) 38 | 39 | return &uninstallCmd 40 | } 41 | -------------------------------------------------------------------------------- /cmd/validate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | _ "embed" 5 | 6 | "github.com/spf13/cobra" 7 | 8 | "github.com/evilmartians/lefthook/internal/lefthook" 9 | ) 10 | 11 | type validate struct{} 12 | 13 | func (validate) New(opts *lefthook.Options) *cobra.Command { 14 | return &cobra.Command{ 15 | Use: "validate", 16 | Short: "Validate lefthook config", 17 | Long: addDoc, 18 | Example: "lefthook validate", 19 | Args: cobra.NoArgs, 20 | RunE: func(_cmd *cobra.Command, _args []string) error { 21 | return lefthook.Validate(opts) 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/evilmartians/lefthook/internal/lefthook" 7 | "github.com/evilmartians/lefthook/internal/log" 8 | ver "github.com/evilmartians/lefthook/internal/version" 9 | ) 10 | 11 | type version struct{} 12 | 13 | func (version) New(_opts *lefthook.Options) *cobra.Command { 14 | var verbose bool 15 | 16 | versionCmd := cobra.Command{ 17 | Use: "version", 18 | Short: "Show lefthook version", 19 | ValidArgsFunction: cobra.NoFileCompletions, 20 | Args: cobra.NoArgs, 21 | Run: func(cmd *cobra.Command, args []string) { 22 | log.Println(ver.Version(verbose)) 23 | }, 24 | } 25 | 26 | versionCmd.Flags().BoolVarP( 27 | &verbose, "full", "f", false, 28 | "full version with commit hash", 29 | ) 30 | 31 | return &versionCmd 32 | } 33 | -------------------------------------------------------------------------------- /docs/configuration.md: -------------------------------------------------------------------------------- 1 | # Lefthook configuration 2 | 3 | > [!IMPORTANT] 4 | > 5 | > This documentation was moved to https://lefthook.dev/configuration/ 6 | -------------------------------------------------------------------------------- /docs/install.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | > [!IMPORTANT] 4 | > 5 | > This documentation was moved to https://lefthook.dev/installation/ 6 | 7 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/Commands.md: -------------------------------------------------------------------------------- 1 | ## `commands` 2 | 3 | Commands to be executed for the hook. Each command has a name and associated run [options](#command). 4 | 5 | **Example** 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | commands: 12 | lint: 13 | ... # command options 14 | ``` 15 | 16 | ### Command options 17 | 18 | - [`run`](./run.md) 19 | - [`skip`](./skip.md) 20 | - [`only`](./only.md) 21 | - [`tags`](./tags.md) 22 | - [`glob`](./glob.md) 23 | - [`files`](./files.md) 24 | - [`file_types`](./file_types.md) 25 | - [`env`](./env.md) 26 | - [`root`](./root.md) 27 | - [`exclude`](./exclude.md) 28 | - [`fail_text`](./fail_text.md) 29 | - [`stage_fixed`](./stage_fixed.md) 30 | - [`interactive`](./interactive.md) 31 | - [`use_stdin`](./use_stdin.md) 32 | - [`priority`](./priority.md) 33 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/Hook.md: -------------------------------------------------------------------------------- 1 | ## Git hook 2 | 3 | Contains settings for the git hook (commands, scripts, skip rules, etc.). You can specify any Git hook or your own custom, e.g. `test` 4 | 5 | ### Hook options 6 | 7 | - [`files`](./files-global.md) 8 | - [`parallel`](./parallel.md) 9 | - [`piped`](./piped.md) 10 | - [`follow`](./follow.md) 11 | - [`exclude_tags`](./exclude_tags.md) 12 | - [`skip`](./skip.md) 13 | - [`only`](./only.md) 14 | - [`jobs`](./jobs.md) 15 | - [`name`](./name.md) 16 | - [`run`](./run.md) 17 | - [`script`](./script.md) 18 | - [`runner`](./runner.md) 19 | - [`group`](./group.md) 20 | - [`parallel`](./parallel.md) 21 | - [`piped`](./piped.md) 22 | - [`jobs`](./jobs.md) 23 | - [`skip`](./skip.md) 24 | - [`only`](./only.md) 25 | - [`tags`](./tags.md) 26 | - [`glob`](./glob.md) 27 | - [`files`](./files.md) 28 | - [`file_types`](./file_types.md) 29 | - [`env`](./env.md) 30 | - [`root`](./root.md) 31 | - [`exclude`](./exclude.md) 32 | - [`fail_text`](./fail_text.md) 33 | - [`stage_fixed`](./stage_fixed.md) 34 | - [`interactive`](./interactive.md) 35 | - [`use_stdin`](./use_stdin.md) 36 | - [`commands`](./Commands.md) 37 | - [`run`](./run.md) 38 | - [`skip`](./skip.md) 39 | - [`only`](./only.md) 40 | - [`tags`](./tags.md) 41 | - [`glob`](./glob.md) 42 | - [`files`](./files.md) 43 | - [`file_types`](./file_types.md) 44 | - [`env`](./env.md) 45 | - [`root`](./root.md) 46 | - [`exclude`](./exclude.md) 47 | - [`fail_text`](./fail_text.md) 48 | - [`stage_fixed`](./stage_fixed.md) 49 | - [`interactive`](./interactive.md) 50 | - [`use_stdin`](./use_stdin.md) 51 | - [`priority`](./priority.md) 52 | - [`scripts`](./Scripts.md) 53 | - [`runner`](./runner.md) 54 | - [`skip`](./skip.md) 55 | - [`only`](./only.md) 56 | - [`tags`](./tags.md) 57 | - [`env`](./env.md) 58 | - [`fail_text`](./fail_text.md) 59 | - [`stage_fixed`](./stage_fixed.md) 60 | - [`interactive`](./interactive.md) 61 | - [`use_stdin`](./use_stdin.md) 62 | - [`priority`](./priority.md) 63 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/Scripts.md: -------------------------------------------------------------------------------- 1 | ## Scripts 2 | 3 | Scripts are stored under `//` folder. These scripts are your own executables which are being run in the project root. 4 | 5 | To add a script for a `pre-commit` hook: 6 | 7 | 1. Run `lefthook add -d pre-commit` 8 | 1. Edit `.lefthook/pre-commit/my-script.sh` 9 | 1. Add an entry to `lefthook.yml` 10 | ```yml 11 | # lefthook.yml 12 | 13 | pre-commit: 14 | scripts: 15 | "my-script.sh": 16 | runner: bash 17 | ``` 18 | 19 | ### Script options 20 | 21 | - [`runner`](./runner.md) 22 | - [`skip`](./skip.md) 23 | - [`only`](./only.md) 24 | - [`tags`](./tags.md) 25 | - [`env`](./env.md) 26 | - [`fail_text`](./fail_text.md) 27 | - [`stage_fixed`](./stage_fixed.md) 28 | - [`interactive`](./interactive.md) 29 | - [`use_stdin`](./use_stdin.md) 30 | - [`priority`](./priority.md) 31 | 32 | ### Example 33 | 34 | Let's create a bash script to check commit templates `.lefthook/commit-msg/template_checker`: 35 | 36 | ```bash 37 | INPUT_FILE=$1 38 | START_LINE=`head -n1 $INPUT_FILE` 39 | PATTERN="^(TICKET)-[[:digit:]]+: " 40 | if ! [[ "$START_LINE" =~ $PATTERN ]]; then 41 | echo "Bad commit message, see example: TICKET-123: some text" 42 | exit 1 43 | fi 44 | ``` 45 | 46 | Now we can ask lefthook to run our bash script by adding this code to 47 | `lefthook.yml` file: 48 | 49 | ```yml 50 | # lefthook.yml 51 | 52 | commit-msg: 53 | scripts: 54 | "template_checker": 55 | runner: bash 56 | ``` 57 | 58 | When you try to commit `git commit -m "bad commit text"` script `template_checker` will be executed. Since commit text doesn't match the described pattern the commit process will be interrupted. 59 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/assert_lefthook_installed.md: -------------------------------------------------------------------------------- 1 | ## `assert_lefthook_installed` 2 | 3 | **Default: `false`** 4 | 5 | When set to `true`, fail (with exit status 1) if `lefthook` executable can't be found in $PATH, under node_modules/, as a Ruby gem, or other supported method. This makes sure git hook won't omit `lefthook` rules if `lefthook` ever was installed. 6 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/colors.md: -------------------------------------------------------------------------------- 1 | ## `colors` 2 | 3 | **Default: `auto`** 4 | 5 | Whether enable or disable colorful output of Lefthook. This option can be overwritten with `--colors` option. You can also provide your own color codes. 6 | 7 | **Example** 8 | 9 | Disable colors. 10 | 11 | ```yml 12 | # lefthook.yml 13 | 14 | colors: false 15 | ``` 16 | 17 | Custom color codes. Can be hex or ANSI codes. 18 | 19 | ```yml 20 | # lefthook.yml 21 | 22 | colors: 23 | cyan: 14 24 | gray: 244 25 | green: '#32CD32' 26 | red: '#FF1493' 27 | yellow: '#F0E68C' 28 | ``` 29 | 30 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/configs.md: -------------------------------------------------------------------------------- 1 | ## `configs` 2 | 3 | **Default:** `[lefthook.yml]` 4 | 5 | An optional array of config paths from remote's root. 6 | 7 | **Example** 8 | 9 | ```yml 10 | # lefthook.yml 11 | 12 | remotes: 13 | - git_url: git@github.com:evilmartians/lefthook 14 | ref: v1.0.0 15 | configs: 16 | - examples/ruby-linter.yml 17 | - examples/test.yml 18 | ``` 19 | 20 | Example with multiple remotes merging multiple configurations. 21 | 22 | ```yml 23 | # lefthook.yml 24 | 25 | remotes: 26 | - git_url: git@github.com:org/lefthook-configs 27 | ref: v1.0.0 28 | configs: 29 | - examples/ruby-linter.yml 30 | - examples/test.yml 31 | - git_url: https://github.com/org2/lefthook-configs 32 | configs: 33 | - lefthooks/pre_commit.yml 34 | - lefthooks/post_merge.yml 35 | - git_url: https://github.com/org3/lefthook-configs 36 | ref: feature/new 37 | configs: 38 | - configs/pre-push.yml 39 | 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/env.md: -------------------------------------------------------------------------------- 1 | ## `env` 2 | 3 | You can specify some ENV variables for the command or script. 4 | 5 | **Example** 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | commands: 12 | test: 13 | env: 14 | RAILS_ENV: test 15 | run: bundle exec rspec 16 | ``` 17 | 18 | #### Extending PATH 19 | 20 | If your hook is run by GUI program, and you use some PATH tweaks in your ~/.rc, you might see an error saying *executable not found*. In that case You can extend the **$PATH** variable with `lefthook-local.yml` configuration the following way. 21 | 22 | ```yml 23 | # lefthook.yml 24 | 25 | pre-commit: 26 | commands: 27 | test: 28 | run: yarn test 29 | ``` 30 | 31 | ```yml 32 | # lefthook-local.yml 33 | 34 | pre-commit: 35 | commands: 36 | test: 37 | env: 38 | PATH: $PATH:/home/me/path/to/yarn 39 | ``` 40 | 41 | **Notes** 42 | 43 | This option is useful when using lefthook on different OSes or shells where ENV variables are set in different ways. 44 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/exclude.md: -------------------------------------------------------------------------------- 1 | ## `exclude` 2 | 3 | For the `exclude` option two variants are supported: 4 | 5 | - A list of globs to be excluded 6 | - A single regular expression (deprecated) 7 | 8 | 9 | > **Note:** The regular expression is matched against full paths to files in the repo, 10 | > relative to the repo root, using `/` as the directory separator on all platforms. 11 | > File paths do not begin with the separator or any other prefix. 12 | 13 | **Example** 14 | 15 | Run Rubocop on staged files with `.rb` extension except for `application.rb`, `routes.rb`, `rails_helper.rb`, and all Ruby files in `config/initializers/`. 16 | 17 | ```yml 18 | # lefthook.yml 19 | 20 | pre-commit: 21 | commands: 22 | lint: 23 | glob: "*.rb" 24 | exclude: 25 | - config/routes.rb 26 | - config/application.rb 27 | - config/initializers/*.rb 28 | - spec/rails_helper.rb 29 | run: bundle exec rubocop --force-exclusion {staged_files} 30 | ``` 31 | 32 | The same example using a regular expression. 33 | 34 | ```yml 35 | # lefthook.yml 36 | 37 | pre-commit: 38 | commands: 39 | lint: 40 | glob: "*.rb" 41 | exclude: '(^|/)(application|routes|rails_helper|initializers/\w+)\.rb$' 42 | run: bundle exec rubocop --force-exclusion {staged_files} 43 | ``` 44 | 45 | **Important** 46 | 47 | Be careful with the config file format's string quoting and escaping rules when writing regexps in it. For YAML, single quotes are often the simplest choice. 48 | 49 | If you've specified `exclude` but don't have a files template in [`run`](./run.md) option, lefthook will check `{staged_files}` for `pre-commit` hook and `{push_files}` for `pre-push` hook and apply filtering. If no files left, the command will be skipped. 50 | 51 | ```yml 52 | # lefthook.yml 53 | 54 | pre-commit: 55 | commands: 56 | lint: 57 | exclude: '(^|/)application\.rb$' 58 | run: bundle exec rubocop # skipped if only application.rb was staged 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/exclude_tags.md: -------------------------------------------------------------------------------- 1 | ## `exclude_tags` 2 | 3 | [Tags](./tags.md) or command names that you want to exclude. This option can be overwritten with `LEFTHOOK_EXCLUDE` env variable. 4 | 5 | **Example** 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | exclude_tags: frontend 12 | commands: 13 | lint: 14 | tags: frontend 15 | ... 16 | test: 17 | tags: frontend 18 | ... 19 | check-syntax: 20 | tags: documentation 21 | ``` 22 | 23 | ```bash 24 | lefthook run pre-commit # will only run check-syntax command 25 | ``` 26 | 27 | **Notes** 28 | 29 | This option is good to specify in `lefthook-local.yml` when you want to skip some execution locally. 30 | 31 | ```yml 32 | # lefthook.yml 33 | 34 | pre-push: 35 | commands: 36 | packages-audit: 37 | tags: 38 | - frontend 39 | - security 40 | run: yarn audit 41 | gems-audit: 42 | tags: 43 | - backend 44 | - security 45 | run: bundle audit 46 | ``` 47 | 48 | You can skip commands by tags: 49 | 50 | ```yml 51 | # lefthook-local.yml 52 | 53 | pre-push: 54 | exclude_tags: 55 | - frontend 56 | ``` 57 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/extends.md: -------------------------------------------------------------------------------- 1 | ## `extends` 2 | 3 | You can extend your config with another one YAML file. Its content will be merged. Extends for `lefthook.yml`, `lefthook-local.yml`, and [`remotes`](./remotes.md) configs are handled separately, so you can have different extends in these files. 4 | 5 | You can use asterisk to make a glob. 6 | 7 | **Example** 8 | 9 | ```yml 10 | # lefthook.yml 11 | 12 | extends: 13 | - /home/user/work/lefthook-extend.yml 14 | - /home/user/work/lefthook-extend-2.yml 15 | - lefthook-extends/file.yml 16 | - ../extend.yml 17 | - projects/*/specific-lefthook-config.yml 18 | ``` 19 | 20 | > The extends will be merged to the main configuration in your file. Here is the order of settings applied: 21 | > 22 | > - `lefthook.yml` – main config file 23 | > - `extends` – configs specified in [extends](./extends.md) option 24 | > - `remotes` – configs specified in [remotes](./remotes.md) option 25 | > - `lefthook-local.yml` – local config file 26 | > 27 | > So, `extends` override settings from `lefthook.yml`, `remotes` override `extends`, and `lefthook-local.yml` can override everything. 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/fail_text.md: -------------------------------------------------------------------------------- 1 | ## `fail_text` 2 | 3 | You can specify a text to show when the command or script fails. 4 | 5 | **Example** 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | commands: 12 | lint: 13 | run: yarn lint 14 | fail_text: Add node executable to $PATH 15 | ``` 16 | 17 | ```bash 18 | $ git commit -m 'fix: Some bug' 19 | 20 | Lefthook v1.1.3 21 | RUNNING HOOK: pre-commit 22 | 23 | EXECUTE > lint 24 | 25 | SUMMARY: (done in 0.01 seconds) 26 | 🥊 lint: Add node executable to $PATH env 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/file_types.md: -------------------------------------------------------------------------------- 1 | ## `file_types` 2 | 3 | Filter files in a [`run`](./run.md) templates by their type. Supported types: 4 | 5 | |File type| Exlanation| 6 | |---------|-----------| 7 | |`text` | Any file that contains text. Symlinks are not followed. | 8 | |`binary` | Any file that contains non-text bytes. Symlinks are not followed. | 9 | |`executable` | Any file that has executable bits set. Symlinks are not followed. | 10 | |`not executable` | Any file without executable bits in file mode. Symlinks included. | 11 | |`symlink` | A symlink file. | 12 | |`not symlink` | Any non-symlink file. | 13 | 14 | > **Important:** When passed multiple file types all constraints will be applied to the resulting list of files 15 | 16 | **Examples** 17 | 18 | Apply some different linters on text and binary files. 19 | 20 | ```yml 21 | # lefthook.yml 22 | 23 | pre-commit: 24 | commands: 25 | lint-code: 26 | run: yarn lint {staged_files} 27 | file_types: text 28 | check-hex-codes: 29 | run: yarn check-hex {staged_files} 30 | file_types: binary 31 | ``` 32 | 33 | Skip symlinks. 34 | 35 | ```yml 36 | # lefthook.yml 37 | 38 | pre-commit: 39 | commands: 40 | lint: 41 | run: yarn lint --fix {staged_files} 42 | file_types: 43 | - not symlink 44 | ``` 45 | 46 | Lint executable scripts. 47 | 48 | ```yml 49 | # lefthook.yml 50 | 51 | pre-commit: 52 | commands: 53 | lint: 54 | run: yarn lint --fix {staged_files} 55 | file_types: 56 | - executable 57 | - text 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/files-global.md: -------------------------------------------------------------------------------- 1 | ## `files` (global) 2 | 3 | A custom git command for files to be referenced in `{files}` template. See [`run`](#run) and [`files`](#files). 4 | 5 | If the result of this command is empty, the execution of commands will be skipped. 6 | 7 | **Example** 8 | 9 | ```yml 10 | # lefthook.yml 11 | 12 | pre-commit: 13 | files: git diff --name-only master # custom list of files 14 | commands: 15 | ... 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/files.md: -------------------------------------------------------------------------------- 1 | ## `files` 2 | 3 | A custom git command for files or directories to be referenced in `{files}` template for [`run`](./run.md) setting. 4 | 5 | If the result of this command is empty, the execution of commands will be skipped. 6 | 7 | This option overwrites the [hook-level `files`](./files-global.md) option. 8 | 9 | **Example** 10 | 11 | Provide a git command to list files. 12 | 13 | ```yml 14 | # lefthook.yml 15 | 16 | pre-push: 17 | commands: 18 | stylelint: 19 | tags: 20 | - frontend 21 | - style 22 | files: git diff --name-only master 23 | glob: "*.js" 24 | run: yarn stylelint {files} 25 | ``` 26 | 27 | Call a custom script for listing files. 28 | 29 | ```yml 30 | # lefthook.yml 31 | 32 | pre-push: 33 | commands: 34 | rubocop: 35 | tags: backend 36 | glob: "**/*.rb" 37 | files: node ./lefthook-scripts/ls-files.js # you can call your own scripts 38 | run: bundle exec rubocop --force-exclusion --parallel {files} 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/follow.md: -------------------------------------------------------------------------------- 1 | ## `follow` 2 | 3 | **Default: `false`** 4 | 5 | Follow the STDOUT of the running commands and scripts. 6 | 7 | **Example** 8 | 9 | ```yml 10 | # lefthook.yml 11 | 12 | pre-push: 13 | follow: true 14 | commands: 15 | backend-tests: 16 | run: bundle exec rspec 17 | frontend-tests: 18 | run: yarn test 19 | ``` 20 | 21 | > **Note:** If used with [`parallel`](#parallel) the output can be a mess, so please avoid setting both options to `true` 22 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/git_url.md: -------------------------------------------------------------------------------- 1 | ## `git_url` 2 | 3 | A URL to Git repository. It will be accessed with privileges of the machine lefthook runs on. 4 | 5 | **Example** 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | remotes: 11 | - git_url: git@github.com:evilmartians/lefthook 12 | ``` 13 | 14 | Or 15 | 16 | ```yml 17 | # lefthook.yml 18 | 19 | remotes: 20 | - git_url: https://github.com/evilmartians/lefthook 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/glob.md: -------------------------------------------------------------------------------- 1 | ## `glob` 2 | 3 | You can set a glob to filter files for your command. This is only used if you use a file template in [`run`](./run.md) option or provide your custom [`files`](./files.md) command. 4 | 5 | **Example** 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | jobs: 12 | - name: lint 13 | run: yarn eslint {staged_files} 14 | glob: "*.{js,ts,jsx,tsx}" 15 | ``` 16 | 17 | > **Note:** from lefthook version `1.10.10` you can also provide a list of globs: 18 | > 19 | > ```yml 20 | > # lefthook.yml 21 | > 22 | > pre-commit: 23 | > jobs: 24 | > - run: yarn lint {staged_files} 25 | > glob: 26 | > - "*.ts" 27 | > - "*.js" 28 | > ``` 29 | 30 | **Notes** 31 | 32 | For patterns that you can use see [this](https://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm) reference. We use [glob](https://github.com/gobwas/glob) library. 33 | 34 | ***When using `root:`*** 35 | 36 | Globs are still calculated from the actual root of the git repo, `root` is ignored. 37 | 38 | ***Behaviour of `**`*** 39 | 40 | Note that the behaviour of `**` is different from typical glob implementations, like `ls` or tools like `lint-staged` in that a double-asterisk matches 1+ directories deep, not zero or more directories. 41 | If you want to match *both* files at the top level and nested, then rather than: 42 | 43 | ```yaml 44 | glob: "src/**/*.js" 45 | ``` 46 | 47 | You'll need: 48 | 49 | ```yaml 50 | glob: "src/*.js" 51 | ``` 52 | 53 | ***Using `glob` without a files template in`run`*** 54 | 55 | If you've specified `glob` but don't have a files template in [`run`](./run.md) option, lefthook will check `{staged_files}` for `pre-commit` hook and `{push_files}` for `pre-push` hook and apply filtering. If no files left, the command will be skipped. 56 | 57 | ```yml 58 | # lefthook.yml 59 | 60 | pre-commit: 61 | jobs: 62 | - name: lint 63 | run: npm run lint # skipped if no .js files staged 64 | glob: "*.js" 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/group.md: -------------------------------------------------------------------------------- 1 | ## `group` 2 | 3 | You can define a group of jobs and configure how they should execute using the following options: 4 | 5 | - [`parallel`](./parallel.md): Executes all jobs in the group simultaneously. 6 | - [`piped`](./piped.md): Executes jobs sequentially, passing output between them. 7 | - [`jobs`](./jobs.md): Specifies the jobs within the group. 8 | 9 | ### Example 10 | 11 | ```yml 12 | # lefthook.yml 13 | 14 | pre-commit: 15 | jobs: 16 | - group: 17 | parallel: true 18 | jobs: 19 | - run: echo 1 20 | - run: echo 2 21 | - run: echo 3 22 | ``` 23 | 24 | > **Note:** To make a group mergeable with settings defined in local config or extends you have to specify the name of the job group belongs to: 25 | > ```yml 26 | > pre-commit: 27 | > jobs: 28 | > - name: a name of a group 29 | > group: 30 | > jobs: 31 | > - name: lint 32 | > run: yarn lint 33 | > - name: test 34 | > run: yarn test 35 | > ``` 36 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/interactive.md: -------------------------------------------------------------------------------- 1 | ## `interactive` 2 | 3 | **Default: `false`** 4 | 5 | > **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.md) option instead. 6 | 7 | 8 | Whether to use interactive mode. This applies the certain behavior: 9 | - All `interactive` commands/scripts are executed after non-interactive. Exception: [`piped`](./piped.md) option is set to `true`. 10 | - When executing, lefthook tries to open /dev/tty (Linux/Unix only) and use it as stdin. 11 | - When [`no_tty`](./no_tty.md) option is set, `interactive` is ignored. 12 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/jobs.md: -------------------------------------------------------------------------------- 1 | ## `jobs` 2 | 3 | > Added in lefthook `1.10.0` 4 | 5 | Jobs provide a flexible way to define tasks, supporting both commands and scripts. Jobs can be grouped for advanced flow control. 6 | 7 | ### Basic example 8 | 9 | Define jobs in your `lefthook.yml` file under a specific hook like `pre-commit`: 10 | 11 | ```yml 12 | # lefthook.yml 13 | 14 | pre-commit: 15 | jobs: 16 | - run: yarn lint 17 | - run: yarn test 18 | ``` 19 | 20 | ### Differences from Commands and Scripts 21 | 22 | **Optional Job Names** 23 | 24 | - Named jobs are merged across [`extends`](./extends.md) and local config. 25 | - Unnamed jobs are appended in the order of their definition. 26 | 27 | **Job Groups** 28 | 29 | - Groups can include other jobs. 30 | - Flow within groups can be parallel or piped. Options `glob`, `root`, and `exclude` apply to all jobs in the group, including nested ones. 31 | 32 | ### Job options 33 | 34 | Below are the available options for configuring jobs. 35 | 36 | - [`name`](./name.md) 37 | - [`run`](./run.md) 38 | - [`script`](./script.md) 39 | - [`runner`](./runner.md) 40 | - [`group`](./group.md) 41 | - [`parallel`](./parallel.md) 42 | - [`piped`](./piped.md) 43 | - [`jobs`](./jobs.md) 44 | - [`skip`](./skip.md) 45 | - [`only`](./only.md) 46 | - [`tags`](./tags.md) 47 | - [`glob`](./glob.md) 48 | - [`files`](./files.md) 49 | - [`file_types`](./file_types.md) 50 | - [`env`](./env.md) 51 | - [`root`](./root.md) 52 | - [`exclude`](./exclude.md) 53 | - [`fail_text`](./fail_text.md) 54 | - [`stage_fixed`](./stage_fixed.md) 55 | - [`interactive`](./interactive.md) 56 | - [`use_stdin`](./use_stdin.md) 57 | 58 | ### Example 59 | 60 | > **Note:** Currently, only `root`, `glob`, and `exclude` options are applied to group jobs. Other options must be set for each job individually. Submit a [feature request](https://github.com/evilmartians/lefthook/issues/new?assignees=&labels=feature+request&projects=&template=feature_request.md) if this limits your workflow. 61 | 62 | A configuration demonstrating a piped group running in parallel with other jobs: 63 | 64 | ```yml 65 | # lefthook.yml 66 | 67 | pre-commit: 68 | parallel: true 69 | jobs: 70 | - name: migrate 71 | root: backend/ 72 | glob: "db/migrations/*" 73 | group: 74 | piped: true 75 | jobs: 76 | - run: bundle install 77 | - run: rails db:migrate 78 | - run: yarn lint --fix {staged_files} 79 | root: frontend/ 80 | stage_fixed: true 81 | - run: bundle exec rubocop 82 | root: backend/ 83 | - run: golangci-lint 84 | root: proxy/ 85 | - script: verify.sh 86 | runner: bash 87 | ``` 88 | 89 | This configuration runs migrate jobs in a piped flow while other jobs execute in parallel. 90 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/lefthook.md: -------------------------------------------------------------------------------- 1 | ## `lefthook` 2 | 3 | **Default:** `null` 4 | 5 | > Added in lefthook `1.10.5` 6 | 7 | Provide a full path to lefthook executable or a command to run lefthook. Bourne shell (`sh`) syntax is supported. 8 | 9 | > **Important:** This option does not merge from `remotes` or `extends` for security reasons. But it gets merged from lefthook local config if specified. 10 | 11 | There are three reasons you may want to specify `lefthook`: 12 | 13 | 1. You want to force using specific lefthook version from your dependencies (e.g. npm package) 14 | 1. You use PnP loader for your JS/TS project, and your `package.json` with lefthook dependency locates in a subfolder 15 | 1. You want to make sure you use concrete lefthook executable path and want to defined it in `lefthook-local.yml` 16 | 17 | ### Examples 18 | 19 | #### Specify lefthook executable 20 | 21 | ```yml 22 | # lefthook.yml 23 | 24 | lefthook: /usr/bin/lefthook 25 | 26 | pre-commit: 27 | jobs: 28 | - run: yarn lint 29 | ``` 30 | 31 | #### Specify a command to run lefthook 32 | 33 | ```yml 34 | # lefthook.yml 35 | 36 | lefthook: | 37 | cd project-with-lefthook 38 | pnpm lefthook 39 | 40 | pre-commit: 41 | jobs: 42 | - run: yarn lint 43 | root: project-with-lefthook 44 | ``` 45 | 46 | #### Force using a version from Rubygems 47 | 48 | ```yml 49 | # lefthook.yml 50 | 51 | lefthook: bundle exec lefthook 52 | 53 | pre-commit: 54 | jobs: 55 | - run: bundle exec rubocop {staged_files} 56 | ``` 57 | 58 | #### Enable debug logs 59 | 60 | ```yml 61 | # lefthook-local.yml 62 | 63 | lefthook: lefthook --verbose 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/min_version.md: -------------------------------------------------------------------------------- 1 | ## `min_version` 2 | 3 | If you want to specify a minimum version for lefthook binary (e.g. if you need some features older versions don't have) you can set this option. 4 | 5 | **Example** 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | min_version: 1.1.3 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/name.md: -------------------------------------------------------------------------------- 1 | ## `name` 2 | 3 | Name of a job. Will be printed in summary. If specified, the jobs can be merged with a jobs of the same name in a [local config](../examples/lefthook-local.md) or [extends](./extends.md). 4 | 5 | ### Example 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | jobs: 12 | - name: lint and fix 13 | run: yarn run eslint --fix {staged_files} 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/no_tty.md: -------------------------------------------------------------------------------- 1 | ## `no_tty` 2 | 3 | **Default: `false`** 4 | 5 | Whether hide spinner and other interactive things. This can be also controlled with `--no-tty` option for `lefthook run` command. 6 | 7 | **Example** 8 | 9 | ```yml 10 | # lefthook.yml 11 | 12 | no_tty: true 13 | ``` 14 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/only.md: -------------------------------------------------------------------------------- 1 | ## `only` 2 | 3 | You can force a command, script, or the whole hook to execute only in certain conditions. This option acts like the opposite of [`skip`](./skip.md). It accepts the same values but skips execution only if the condition is not satisfied. 4 | 5 | > **Note:** `skip` option takes precedence over `only` option, so if you have conflicting conditions the execution will be skipped. 6 | 7 | **Example** 8 | 9 | Execute a hook only for `dev/*` branches. 10 | 11 | ```yml 12 | # lefthook.yml 13 | 14 | pre-commit: 15 | only: 16 | - ref: dev/* 17 | commands: 18 | lint: 19 | run: yarn lint 20 | test: 21 | run: yarn test 22 | ``` 23 | 24 | When rebasing execute quick linter but skip usual linter and tests. 25 | 26 | ```yml 27 | # lefthook.yml 28 | 29 | pre-commit: 30 | commands: 31 | lint: 32 | skip: rebase 33 | run: yarn lint 34 | test: 35 | skip: rebase 36 | run: yarn test 37 | lint-on-rebase: 38 | only: rebase 39 | run: yarn lint-quickly 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/output.md: -------------------------------------------------------------------------------- 1 | ## `output` 2 | 3 | You can manage verbosity using the `output` config. You can specify what to print in your output by setting these values, which you need to have 4 | 5 | Possible values are `meta,summary,success,failure,execution,execution_out,execution_info,skips`. 6 | By default, all output values are enabled 7 | 8 | You can also disable all output with setting `output: false`. In this case only errors will be printed. 9 | 10 | This config quiets all outputs except for errors. 11 | 12 | `output` is enabled if there is no `skip_output` and `LEFTHOOK_QUIET`. 13 | 14 | **Example** 15 | 16 | ```yml 17 | # lefthook.yml 18 | 19 | output: 20 | - meta # Print lefthook version 21 | - summary # Print summary block (successful and failed steps) 22 | - empty_summary # Print summary heading when there are no steps to run 23 | - success # Print successful steps 24 | - failure # Print failed steps printing 25 | - execution # Print any execution logs 26 | - execution_out # Print execution output 27 | - execution_info # Print `EXECUTE > ...` logging 28 | - skips # Print "skip" (i.e. no files matched) 29 | ``` 30 | 31 | You can also *extend* this list with an environment variable `LEFTHOOK_OUTPUT`: 32 | 33 | ```bash 34 | LEFTHOOK_OUTPUT="meta,success,summary" lefthook run pre-commit 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/parallel.md: -------------------------------------------------------------------------------- 1 | ## `parallel` 2 | 3 | **Default: `false`** 4 | 5 | > **Note:** Lefthook runs commands and scripts **sequentially** by default 6 | 7 | Run commands and scripts concurrently. 8 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/piped.md: -------------------------------------------------------------------------------- 1 | ## `piped` 2 | 3 | **Default: `false`** 4 | 5 | > **Note:** Lefthook will return an error if both `piped: true` and `parallel: true` are set 6 | 7 | Stop running commands and scripts if one of them fail. 8 | 9 | **Example** 10 | 11 | ```yml 12 | # lefthook.yml 13 | 14 | database: 15 | piped: true # Stop if one of the steps fail 16 | commands: 17 | 1_create: 18 | run: rake db:create 19 | 2_migrate: 20 | run: rake db:migrate 21 | 3_seed: 22 | run: rake db:seed 23 | ``` 24 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/priority.md: -------------------------------------------------------------------------------- 1 | ## `priority` 2 | 3 | **Default: `0`** 4 | 5 | > **Note:** This option makes sense only when `parallel: false` or `piped: true` is set. 6 | > 7 | > Value `0` is considered an `+Infinity`, so commands or scripts with `priority: 0` or without this setting will be run at the very end. 8 | 9 | Set priority from 1 to +Infinity. This option can be used to configure the order of the sequential steps. 10 | 11 | **Example** 12 | 13 | ```yml 14 | # lefthook.yml 15 | 16 | post-checkout: 17 | piped: true 18 | commands: 19 | db-create: 20 | priority: 1 21 | run: rails db:create 22 | db-migrate: 23 | priority: 2 24 | run: rails db:migrate 25 | db-seed: 26 | priority: 3 27 | run: rails db:seed 28 | 29 | scripts: 30 | "check-spelling.sh": 31 | runner: bash 32 | priority: 1 33 | "check-grammar.rb": 34 | runner: ruby 35 | priority: 2 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/rc.md: -------------------------------------------------------------------------------- 1 | ## `rc` 2 | 3 | Provide an [**rc**](https://www.baeldung.com/linux/rc-files) file, which is actually a simple `sh` script. Currently it can be used to set ENV variables that are not accessible from non-shell programs. 4 | 5 | **Example** 6 | 7 | Use cases: 8 | 9 | - You have a GUI program that runs git hooks (e.g., VSCode) 10 | - You reference executables that are accessible only from a tweaked $PATH environment variable (e.g., when using rbenv or nvm, fnm) 11 | - Or even if your GUI program cannot locate the `lefthook` executable :scream: 12 | - Or if you want to use ENV variables that control the executables behavior in `lefthook.yml` 13 | 14 | ```bash 15 | # An npm executable which is managed by nvm 16 | $ which npm 17 | /home/user/.nvm/versions/node/v15.14.0/bin/npm 18 | ``` 19 | 20 | ```yml 21 | # lefthook.yml 22 | 23 | pre-commit: 24 | commands: 25 | lint: 26 | run: npm run eslint {staged_files} 27 | ``` 28 | 29 | Provide a tweak to access `npm` executable the same way you do it in your ~/rc. 30 | 31 | ```yml 32 | # lefthook-local.yml 33 | 34 | # You can choose whatever name you want. 35 | # You can share it between projects where you use lefthook. 36 | # Make sure the path is absolute. 37 | rc: ~/.lefthookrc 38 | ``` 39 | 40 | Or 41 | 42 | ```yml 43 | # lefthook-local.yml 44 | 45 | # If the path contains spaces, you need to quote it. 46 | rc: '"${XDG_CONFIG_HOME:-$HOME/.config}/lefthookrc"' 47 | ``` 48 | 49 | In the rc file, export any new environment variables or modify existing ones. 50 | 51 | ```bash 52 | # ~/.lefthookrc 53 | 54 | # An nvm way 55 | export NVM_DIR="$HOME/.nvm" 56 | [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" 57 | 58 | # An fnm way 59 | export FNM_DIR="$HOME/.fnm" 60 | [ -s "$FNM_DIR/fnm.sh" ] && \. "$FNM_DIR/fnm.sh" 61 | 62 | # Or maybe just 63 | PATH=$PATH:$HOME/.nvm/versions/node/v15.14.0/bin 64 | ``` 65 | 66 | ```bash 67 | # Make sure you updated git hooks. This is important. 68 | $ lefthook install -f 69 | ``` 70 | 71 | Now any program that runs your hooks will have a tweaked PATH environment variable and will be able to get `nvm` :wink: 72 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/ref.md: -------------------------------------------------------------------------------- 1 | ## `ref` 2 | 3 | An optional *branch* or *tag* name. 4 | 5 | > **Note:** If you initially had `ref` option, ran `lefthook install`, and then removed it, lefthook won't decide which branch/tag to use as a ref. So, if you added it once, please, use it always to avoid issues in local setups. 6 | 7 | **Example** 8 | 9 | ```yml 10 | # lefthook.yml 11 | 12 | remotes: 13 | - git_url: git@github.com:evilmartians/lefthook 14 | ref: v1.0.0 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/refetch.md: -------------------------------------------------------------------------------- 1 | ## `refetch` 2 | 3 | **Default:** `false` 4 | 5 | Force remote config refetching on every run. Lefthook will be refetching the specified remote every time it is called. 6 | 7 | **Example** 8 | 9 | ```yml 10 | # lefthook.yml 11 | 12 | remotes: 13 | - git_url: https://github.com/evilmartians/lefthook 14 | refetch: true 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/refetch_frequency.md: -------------------------------------------------------------------------------- 1 | ## `refetch_frequency` 2 | 3 | **Default:** Not set 4 | 5 | Specifies how frequently Lefthook should refetch the remote configuration. This can be set to `always`, `never` or a time duration like `24h`, `30m`, etc. 6 | 7 | - When set to `always`, Lefthook will always refetch the remote configuration on each run. 8 | - When set to a duration (e.g., `24h`), Lefthook will check the last fetch time and refetch the configuration only if the specified amount of time has passed. 9 | - When set to `never` or not set, Lefthook will not fetch from remote. 10 | 11 | **Example** 12 | 13 | ```yml 14 | # lefthook.yml 15 | 16 | remotes: 17 | - git_url: https://github.com/evilmartians/lefthook 18 | refetch_frequency: 24h # Refetches once every 24 hours 19 | ``` 20 | 21 | > WARNING 22 | > If `refetch` is set to `true`, it overrides any setting in `refetch_frequency`. 23 | 24 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/remotes.md: -------------------------------------------------------------------------------- 1 | ## `remotes` 2 | 3 | You can provide multiple remote configs if you want to share yours lefthook configurations across many projects. Lefthook will automatically download and merge configurations into your local `lefthook.yml`. 4 | 5 | You can use [`extends`](./extends.md) but the paths must be relative to the remote repository root. 6 | 7 | If you provide [`scripts`](./scripts.md) in a remote config file, the [scripts](./source_dir.md) folder must also be in the **root of the repository**. 8 | 9 | **Note** 10 | 11 | The configuration from `remotes` will be merged to the local config using the following priority: 12 | 13 | 1. Local main config (`lefthook.yml`) 14 | 1. Remote configs (`remotes`) 15 | 1. Local overrides (`lefthook-local.yml`) 16 | 17 | This priority may be changed in the future. For convenience, if you use `remotes`, please don't configure any hooks. 18 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/root.md: -------------------------------------------------------------------------------- 1 | ## `root` 2 | 3 | You can change the CWD for the command you execute using `root` option. 4 | 5 | This is useful when you execute some `npm` or `yarn` command but the `package.json` is in another directory. 6 | 7 | For `pre-push` and `pre-commit` hooks and for the custom `files` command `root` option is used to filter file paths. If all files are filtered the command will be skipped. 8 | 9 | **Example** 10 | 11 | Format and stage files from a `client/` folder. 12 | 13 | ```bash 14 | # Folders structure 15 | 16 | $ tree . 17 | . 18 | ├── client/ 19 | │ ├── package.json 20 | │ ├── node_modules/ 21 | | ├── ... 22 | ├── server/ 23 | | ... 24 | ``` 25 | 26 | ```yml 27 | # lefthook.yml 28 | 29 | pre-commit: 30 | commands: 31 | lint: 32 | root: "client/" 33 | glob: "*.{js,ts}" 34 | run: yarn eslint --fix {staged_files} && git add {staged_files} 35 | ``` 36 | 37 | **Notes** 38 | 39 | ***When using `root:`*** 40 | 41 | Globs are still calculated from the actual root of the git repo, `root` is ignored. 42 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/runner.md: -------------------------------------------------------------------------------- 1 | ## `runner` 2 | 3 | You should specify a runner for the script. This is a command that should execute a script file. It will be called the following way: ` ` (e.g. `ruby .lefthook/pre-commit/lint.rb`). 4 | 5 | **Example** 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | scripts: 12 | "lint.js": 13 | runner: node 14 | "check.go": 15 | runner: go run 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/script.md: -------------------------------------------------------------------------------- 1 | ## `script` 2 | 3 | Name of a script to execute. The rules are the same as for [`scripts`](./Scripts.md) 4 | 5 | ### Example 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | jobs: 12 | - script: linter.sh 13 | runner: bash 14 | ``` 15 | 16 | ```bash 17 | # .lefthook/pre-commit/linter.sh 18 | 19 | echo "Everything is OK" 20 | ``` 21 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/skip.md: -------------------------------------------------------------------------------- 1 | ## `skip` 2 | 3 | You can skip all or specific commands and scripts using `skip` option. You can also skip when merging, rebasing, or being on a specific branch. Globs are available for branches. 4 | 5 | Possible skip values: 6 | - `rebase` - when in rebase git state 7 | - `merge` - when in merge git state 8 | - `merge-commit` - when current HEAD commit is the merge commit 9 | - `ref: main` - when on a `main` branch 10 | - `run: test ${SKIP_ME} -eq 1` - when `test ${SKIP_ME} -eq 1` is successful (return code is 0) 11 | 12 | **Example** 13 | 14 | Always skipping a command: 15 | 16 | ```yml 17 | # lefthook.yml 18 | 19 | pre-commit: 20 | commands: 21 | lint: 22 | skip: true 23 | run: yarn lint 24 | ``` 25 | 26 | Skipping on merging and rebasing: 27 | 28 | ```yml 29 | # lefthook.yml 30 | 31 | pre-commit: 32 | commands: 33 | lint: 34 | skip: 35 | - merge 36 | - rebase 37 | run: yarn lint 38 | ``` 39 | 40 | Or 41 | 42 | ```yml 43 | # lefthook.yml 44 | 45 | pre-commit: 46 | commands: 47 | lint: 48 | skip: merge 49 | run: yarn lint 50 | ``` 51 | 52 | Skipping when your are on a merge commit: 53 | 54 | ```yml 55 | # lefthook.yml 56 | 57 | pre-push: 58 | commands: 59 | lint: 60 | skip: merge-commit 61 | run: yarn lint 62 | ``` 63 | 64 | Skipping the whole hook on `main` branch: 65 | 66 | ```yml 67 | # lefthook.yml 68 | 69 | pre-commit: 70 | skip: 71 | - ref: main 72 | commands: 73 | lint: 74 | run: yarn lint 75 | test: 76 | run: yarn test 77 | ``` 78 | 79 | Skipping hook for all `dev/*` branches: 80 | 81 | ```yml 82 | # lefthook.yml 83 | 84 | pre-commit: 85 | skip: 86 | - ref: dev/* 87 | commands: 88 | lint: 89 | run: yarn lint 90 | test: 91 | run: yarn test 92 | ``` 93 | 94 | Skipping hook by running a command: 95 | 96 | ```yml 97 | # lefthook.yml 98 | 99 | pre-commit: 100 | skip: 101 | - run: test "${NO_HOOK}" -eq 1 102 | commands: 103 | lint: 104 | run: yarn lint 105 | test: 106 | run: yarn test 107 | ``` 108 | 109 | > TIP 110 | > 111 | > Always skipping is useful when you have a `lefthook-local.yml` config and you don't want to run some commands locally. So you just overwrite the `skip` option for them to be `true`. 112 | > 113 | > ```yml 114 | > # lefthook.yml 115 | > 116 | > pre-commit: 117 | > commands: 118 | > lint: 119 | > run: yarn lint 120 | > ``` 121 | > 122 | > ```yml 123 | > # lefthook-local.yml 124 | > 125 | > pre-commit: 126 | > commands: 127 | > lint: 128 | > skip: true 129 | > ``` 130 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/skip_lfs.md: -------------------------------------------------------------------------------- 1 | ## `skip_lfs` 2 | 3 | **Default:** `false` 4 | 5 | Skip running LFS hooks even if it exists on your system. 6 | 7 | ### Example 8 | 9 | ```yml 10 | # lefthook.yml 11 | 12 | skip_lfs: true 13 | 14 | pre-push: 15 | commands: 16 | test: 17 | run: yarn test 18 | ``` 19 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/skip_output.md: -------------------------------------------------------------------------------- 1 | ## `skip_output` 2 | 3 | > **DEPRECATED** This feature is deprecated and might be removed in future versions. Please, use [`output`](./output.md) instead for managing verbosity. 4 | 5 | You can manage the verbosity using the `skip_output` config. You can set whether lefthook should print some parts of its output. 6 | 7 | Possible values are `meta,summary,success,failure,execution,execution_out,execution_info,skips`. 8 | 9 | You can also disable all output with setting `skip_output: true`. In this case only errors will be printed. 10 | 11 | This config quiets all outputs except for errors. 12 | 13 | **Example** 14 | 15 | ```yml 16 | # lefthook.yml 17 | 18 | skip_output: 19 | - meta # Skips lefthook version printing 20 | - summary # Skips summary block (successful and failed steps) printing 21 | - empty_summary # Skips summary heading when there are no steps to run 22 | - success # Skips successful steps printing 23 | - failure # Skips failed steps printing 24 | - execution # Skips printing any execution logs (but prints if the execution failed) 25 | - execution_out # Skips printing execution output (but still prints failed commands output) 26 | - execution_info # Skips printing `EXECUTE > ...` logging 27 | - skips # Skips "skip" printing (i.e. no files matched) 28 | ``` 29 | 30 | You can also *extend* this list with an environment variable `LEFTHOOK_QUIET`: 31 | 32 | ```bash 33 | LEFTHOOK_QUIET="meta,success,summary" lefthook run pre-commit 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/source_dir.md: -------------------------------------------------------------------------------- 1 | ## `source_dir` 2 | 3 | **Default: `.lefthook/`** 4 | 5 | Change a directory for script files. Directory for script files contains folders with git hook names which contain script files. 6 | 7 | Example of directory tree: 8 | 9 | ``` 10 | .lefthook/ 11 | ├── pre-commit/ 12 | │ ├── lint.sh 13 | │ └── test.py 14 | └── pre-push/ 15 | └── check-files.rb 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/source_dir_local.md: -------------------------------------------------------------------------------- 1 | ## `source_dir_local` 2 | 3 | **Default: `.lefthook-local/`** 4 | 5 | Change a directory for *local* script files (not stored in VCS). 6 | 7 | This option is useful if you have a `lefthook-local.yml` config file and want to reference different scripts there. 8 | 9 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/stage_fixed.md: -------------------------------------------------------------------------------- 1 | ## `stage_fixed` 2 | 3 | **Default: `false`** 4 | 5 | > Works **only for `pre-commit`** hook 6 | 7 | When set to `true` lefthook will automatically call `git add` on files after running the command or script. For a command if [`files`](./files.md) option was specified, the specified command will be used to retrieve files for `git add`. For scripts and commands without [`files`](./files.md) option `{staged_files}` template will be used. All filters ([`glob`](./glob.md), [`exclude`](./exclude.md)) will be applied if specified. 8 | 9 | **Example** 10 | 11 | ```yml 12 | # lefthook.yml 13 | 14 | pre-commit: 15 | commands: 16 | lint: 17 | run: npm run lint --fix {staged_files} 18 | stage_fixed: true 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/tags.md: -------------------------------------------------------------------------------- 1 | ## `tags` 2 | 3 | You can specify tags for commands and scripts. This is useful for [excluding](./exclude_tags.md). You can specify more than one tag using comma. 4 | 5 | **Example** 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | commands: 12 | lint: 13 | tags: 14 | - frontend 15 | - js 16 | run: yarn lint 17 | test: 18 | tags: 19 | - backend 20 | - ruby 21 | run: bundle exec rspec 22 | ``` 23 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/templates.md: -------------------------------------------------------------------------------- 1 | ## `templates` 2 | 3 | > Added in lefthook `1.10.8` 4 | 5 | Provide custom replacement for templates in `run` values. 6 | 7 | With `templates` you can specify what can be overridden via `lefthook-local.yml` without a need to overwrite every jobs in your configuration. 8 | 9 | ## Example 10 | 11 | ### Override with lefthook-local.yml 12 | 13 | ```yml 14 | # lefthook.yml 15 | 16 | templates: 17 | dip: # empty 18 | 19 | pre-commit: 20 | jobs: 21 | # Will run: `bundle exec rubocop file1 file2 file3 ...` 22 | - run: {dip} bundle exec rubocop {staged_files} 23 | ``` 24 | 25 | ```yml 26 | # lefthook-local.yml 27 | 28 | templates: 29 | dip: dip # Will run: `dip bundle exec rubocop file1 file2 file3 ...` 30 | ``` 31 | 32 | ### Reduce redundancy 33 | 34 | ```yml 35 | # lefthook.yml 36 | 37 | templates: 38 | wrapper: docker-compose run --rm -v $(pwd):/app service 39 | 40 | pre-commit: 41 | jobs: 42 | - run: {wrapper} yarn format 43 | - run: {wrapper} yarn lint 44 | - run: {wrapper} yarn test 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/mdbook/configuration/use_stdin.md: -------------------------------------------------------------------------------- 1 | ## `use_stdin` 2 | 3 | > **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). 4 | 5 | Pass the stdin from the OS to the command/script. 6 | 7 | **Example** 8 | 9 | Use this option for the `pre-push` hook when you have a script that does `while read ...`. Without this option lefthook will hang: lefthook uses [pseudo TTY](https://github.com/creack/pty) by default, and it doesn't close stdin when all data is read. 10 | 11 | ```bash 12 | # .lefthook/pre-push/do-the-magic.sh 13 | 14 | remote="$1" 15 | url="$2" 16 | 17 | while read local_ref local_oid remote_ref remote_oid; do 18 | # ... 19 | done 20 | ``` 21 | 22 | ```yml 23 | # lefthook.yml 24 | pre-push: 25 | scripts: 26 | "do-the-magic.sh": 27 | runner: bash 28 | use_stdin: true 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/mdbook/contributors.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | -------------------------------------------------------------------------------- /docs/mdbook/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [lefthook-local.yml (with extensions and overrides)](./lefthook-local.md) 4 | - [Apply fixes on pre-commit hook](./stage_fixed.md) 5 | - [Filter files for commands](./filters.md) 6 | - [Skip or run on condition](./skip.md) 7 | - [Use remote config](./remotes.md) 8 | - [Use with commitlint](./commitlint.md) 9 | -------------------------------------------------------------------------------- /docs/mdbook/examples/commitlint.md: -------------------------------------------------------------------------------- 1 | ## Commitlint and commitzen 2 | 3 | Use lefthook to generate commit messages using commitzen and validate them with commitlint. 4 | 5 | ## Install dependencies 6 | 7 | ```bash 8 | yarn add -D @commitlint/cli @commitlint/config-conventional 9 | 10 | # For commitzen 11 | yarn add -D commitizen cz-conventional-changelog 12 | ``` 13 | 14 | ## Configure 15 | 16 | Setup `commitlint.config.js`. Conventional configuration: 17 | 18 | ```js 19 | // commitlint.config.js 20 | 21 | module.exports = {extends: ['@commitlint/config-conventional']}; 22 | ``` 23 | 24 | If you are using commitzen, make sure to add this in `package.json`: 25 | 26 | ```json 27 | "config": { 28 | "commitizen": { 29 | "path": "./node_modules/cz-conventional-changelog" 30 | } 31 | } 32 | ``` 33 | 34 | Configure lefthook: 35 | 36 | ```yml 37 | # lefthook.yml 38 | 39 | # Build commit messages 40 | prepare-commit-msg: 41 | commands: 42 | commitzen: 43 | interactive: true 44 | run: yarn run cz --hook # Or npx cz --hook 45 | env: 46 | LEFTHOOK: 0 47 | 48 | # Validate commit messages 49 | commit-msg: 50 | commands: 51 | "lint commit message": 52 | run: yarn run commitlint --edit {1} 53 | ``` 54 | 55 | 56 | ## Test it 57 | 58 | ```bash 59 | # You can type it without message, if you are using commitzen 60 | git commit 61 | 62 | # Or provide a commit message is using only commitlint 63 | git commit -am 'fix: typo' 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/mdbook/examples/filters.md: -------------------------------------------------------------------------------- 1 | ## Filters 2 | 3 | Files passed to your hooks can be filtered with the following options 4 | 5 | - [`glob`](../configuration/glob.md) 6 | - [`exclude`](../configuration/exclude.md) 7 | - [`file_types`](../configuration/file_types.md) 8 | - [`root`](../configuration/root.md) 9 | 10 | In this example all **staged files** will pass through these filters. 11 | 12 | ```yml 13 | # lefthook.yml 14 | 15 | pre-commit: 16 | commands: 17 | lint: 18 | run: yarn lint {staged_files} --fix 19 | glob: "*.{js,ts}" 20 | root: frontend 21 | exclude: 22 | - *.config.js 23 | - *.config.ts 24 | file_types: 25 | - not executable 26 | ``` 27 | 28 | Imagine you've staged the following files 29 | 30 | ```bash 31 | backend/asset.js 32 | frontend/src/index.ts 33 | frontend/bin/cli.js # <- executable 34 | frontend/eslint.config.js 35 | frontend/README.md 36 | ``` 37 | 38 | After all filters applied the `lint` command will execute the following: 39 | 40 | ```bash 41 | yarn lint frontend/src/index.ts --fix 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/mdbook/examples/lefthook-local.md: -------------------------------------------------------------------------------- 1 | ## lefthook-local.yml 2 | 3 | `lefthook-local.yml` overrides and extends the configuration of your main `lefthook.yml` (or `lefthook.toml`, [etc.](../configuration)) file. 4 | 5 | > **Tip:** You can put `lefthook-local.yml` into your `~/.gitignore`, so in every project you can have your local-only overrides. 6 | 7 | *Special feature* of `lefthook-local.yml`: you can wrap the commands using `{cmd}` template. 8 | 9 | ```yml 10 | # lefthook.yml 11 | 12 | pre-commit: 13 | commands: 14 | lint: 15 | run: bundle exec rubocop {staged_files} 16 | glob: "*.rb" 17 | check-links: 18 | run: lychee {staged_files} 19 | ``` 20 | 21 | ```yml 22 | # lefthook-local.yml 23 | 24 | pre-commit: 25 | parallel: true # run all commands concurrently 26 | commands: 27 | lint: 28 | run: docker-compose run backend {cmd} # wrap the original command with docker-compose 29 | check-links: 30 | skip: true # skip checking links 31 | 32 | # Add another hook 33 | post-merge: 34 | files: "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD" 35 | commands: 36 | dependencies: 37 | glob: "Gemfile*" 38 | run: docker-compose run backend bundle install 39 | ``` 40 | 41 | --- 42 | 43 | ```yml 44 | # The resulting config would look like this 45 | 46 | pre-commit: 47 | parallel: true 48 | commands: 49 | lint: 50 | run: docker-compose run backend bundle exec rubocop {staged_files} 51 | glob: "*.rb" 52 | check-links: 53 | run: lychee {staged_files} 54 | skip: true 55 | 56 | post-merge: 57 | files: "git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD" 58 | commands: 59 | dependencies: 60 | glob: "Gemfile*" 61 | run: docker-compose run backend bundle install 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/mdbook/examples/remotes.md: -------------------------------------------------------------------------------- 1 | ## Remotes 2 | 3 | Use configurations from other Git repositories via `remotes` feature. 4 | 5 | Lefthook will automatically download the remote config files and merge them into existing configuration. 6 | 7 | ```yml 8 | remotes: 9 | - git_url: https://github.com/evilmartians/lefthook 10 | configs: 11 | - examples/remote/ping.yml 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/mdbook/examples/skip.md: -------------------------------------------------------------------------------- 1 | ## Skip or run on condition 2 | 3 | Here are two hooks. 4 | 5 | `pre-commit` hook will only be executed when you're committing something on a branch starting with `def/` prefix. 6 | 7 | In `pre-push` hook: 8 | - `test` command will be skipped if `NO_TEST` env variable is set to `1` 9 | - `lint` command will only be executed if you're pushing the `main` branch 10 | 11 | ```yml 12 | # lefthook.yml 13 | 14 | pre-commit: 15 | only: 16 | - ref: dev/* 17 | commands: 18 | lint: 19 | run: yarn lint {staged_files} --fix 20 | glob: "*.{ts,js}" 21 | test: 22 | run: yarn test 23 | 24 | pre-push: 25 | commands: 26 | test: 27 | run: yarn test 28 | skip: 29 | - run: test "$NO_TEST" -eq 1 30 | lint: 31 | run: yarn lint 32 | only: 33 | - ref: main 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/mdbook/examples/stage_fixed.md: -------------------------------------------------------------------------------- 1 | ## Stage fixed files 2 | 3 | > Works only for `pre-commit` Git hook 4 | 5 | Sometimes your linter fixes the changes and you usually want to commit them automatically. To enable auto-staging of the fixed files use [`stage_fixed`](../configuration/stage_fixed.md) option. 6 | 7 | ```yml 8 | # lefthook.yml 9 | 10 | pre-commit: 11 | commands: 12 | lint: 13 | run: yarn lint {staged_files} --fix 14 | stage_fixed: true 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/mdbook/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/mdbook/installation/README.md: -------------------------------------------------------------------------------- 1 | # Install lefthook 2 | 3 | As a dev dependency 4 | 5 | - [Ruby](./ruby.md) 6 | - [Node.js](./node.md) 7 | - [Swift](./swift.md) 8 | 9 | With package managers 10 | - [Go](./go.md) 11 | - [Python](./python.md) 12 | - [Scoop](./scoop.md) 13 | - [Homebrew](./homebrew.md) 14 | - [Winget](./winget.md) 15 | - [Snap](./snap.md) 16 | - [Debian-based distro](./deb.md) 17 | - [RPM-based distro](./rpm.md) 18 | - [Alpine](./alpine.md) 19 | - [Arch Linux](./arch.md) 20 | - [Mise](./mise.md) 21 | 22 | 23 | [Manual installation](./manual.md) 24 | -------------------------------------------------------------------------------- /docs/mdbook/installation/alpine.md: -------------------------------------------------------------------------------- 1 | ## APK packages for Alpine 2 | 3 | ```sh 4 | sudo apk add --no-cache bash curl 5 | curl -1sLf 'https://dl.cloudsmith.io/public/evilmartians/lefthook/setup.alpine.sh' | sudo -E bash 6 | sudo apk add lefthook 7 | ``` 8 | 9 | See all instructions: https://cloudsmith.io/~evilmartians/repos/lefthook/setup/#formats-alpine 10 | 11 | [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com "RPM package repository hosting is graciously provided by Cloudsmith") 12 | -------------------------------------------------------------------------------- /docs/mdbook/installation/arch.md: -------------------------------------------------------------------------------- 1 | ## AUR for Arch 2 | 3 | - Official [AUR package](https://aur.archlinux.org/packages/lefthook) (compiles from sources) 4 | - Community [AUR package](https://aur.archlinux.org/packages/lefthook-bin) (delivers pre-compiled binaries) 5 | 6 | ```sh 7 | # To compile from sources 8 | yay -S lefthook 9 | 10 | # To install only executable 11 | yay -S lefthook-bin 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/mdbook/installation/deb.md: -------------------------------------------------------------------------------- 1 | ## APT packages for Debian/Ubuntu Linux 2 | 3 | ```sh 4 | curl -1sLf 'https://dl.cloudsmith.io/public/evilmartians/lefthook/setup.deb.sh' | sudo -E bash 5 | sudo apt install lefthook 6 | ``` 7 | See all instructions: https://cloudsmith.io/~evilmartians/repos/lefthook/setup/#formats-deb 8 | 9 | [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com "Debian package repository hosting is graciously provided by Cloudsmith") 10 | 11 | -------------------------------------------------------------------------------- /docs/mdbook/installation/go.md: -------------------------------------------------------------------------------- 1 | ## Go 2 | 3 | ```bash 4 | go install github.com/evilmartians/lefthook@latest 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/mdbook/installation/homebrew.md: -------------------------------------------------------------------------------- 1 | ## Homebrew for MacOS and Linux 2 | 3 | ```bash 4 | brew install lefthook 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/mdbook/installation/manual.md: -------------------------------------------------------------------------------- 1 | ## Manuall installation with prebuilt executable 2 | 3 | Download binaries from [latest release](https://github.com/evilmartians/lefthook/releases/latest) and install manually. 4 | -------------------------------------------------------------------------------- /docs/mdbook/installation/mise.md: -------------------------------------------------------------------------------- 1 | ## Mise 2 | 3 | > See [https://github.com/jdx/mise](https://github.com/jdx/mise) 4 | 5 | ```bash 6 | mise use lefthook@latest 7 | ``` 8 | 9 | **Note**: The mise plugin for lefthook is maintained by the community. While we appreciate their contribution, the lefthook team cannot provide direct support for mise-specific installation issues. 10 | -------------------------------------------------------------------------------- /docs/mdbook/installation/node.md: -------------------------------------------------------------------------------- 1 | ## Node.js 2 | 3 | ```bash 4 | npm install --save-dev lefthook 5 | ``` 6 | 7 | ```bash 8 | yarn add --dev lefthook 9 | ``` 10 | 11 | ```bash 12 | pnpm add -D lefthook 13 | ``` 14 | 15 | > **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. 16 | 17 | **Note**: lefthook has three NPM packages with different ways to deliver the executables 18 | 19 | 1. [lefthook](https://www.npmjs.com/package/lefthook) installs one executable for your system 20 | 21 | ```bash 22 | npm install --save-dev lefthook 23 | ``` 24 | 25 | 1. **legacy**[^1] [@evilmartians/lefthook](https://www.npmjs.com/package/@evilmartians/lefthook) installs executables for all OS 26 | 27 | ```bash 28 | npm install --save-dev @evilmartians/lefthook 29 | ``` 30 | 31 | 1. **legacy**[^1] [@evilmartians/lefthook-installer](https://www.npmjs.com/package/@evilmartians/lefthook-installer) fetches the right executable on installation 32 | 33 | ```bash 34 | npm install --save-dev @evilmartians/lefthook-installer 35 | ``` 36 | [^1]: Legacy distributions are still maintained but they will be shut down in the future. 37 | -------------------------------------------------------------------------------- /docs/mdbook/installation/python.md: -------------------------------------------------------------------------------- 1 | ## Python 2 | 3 | ```sh 4 | python -m pip install --user lefthook 5 | ``` 6 | 7 | ```sh 8 | uv add --dev lefthook 9 | ``` -------------------------------------------------------------------------------- /docs/mdbook/installation/rpm.md: -------------------------------------------------------------------------------- 1 | ## RPM packages for CentOS/Fedora Linux 2 | 3 | ```sh 4 | curl -1sLf 'https://dl.cloudsmith.io/public/evilmartians/lefthook/setup.rpm.sh' | sudo -E bash 5 | sudo yum install lefthook 6 | ``` 7 | 8 | See all instructions: https://cloudsmith.io/~evilmartians/repos/lefthook/setup/#repository-setup-yum 9 | 10 | [![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com "RPM package repository hosting is graciously provided by Cloudsmith") 11 | -------------------------------------------------------------------------------- /docs/mdbook/installation/ruby.md: -------------------------------------------------------------------------------- 1 | ## Ruby 2 | 3 | ```ruby 4 | # Gemfile 5 | 6 | group :development do 7 | gem "lefthook", require: false 8 | end 9 | ``` 10 | 11 | Or globally 12 | 13 | ```bash 14 | gem install lefthook 15 | ``` 16 | 17 | **Troubleshooting** 18 | 19 | If you see the error `lefthook: command not found` you need to check your $PATH. Also try to restart your terminal. 20 | -------------------------------------------------------------------------------- /docs/mdbook/installation/scoop.md: -------------------------------------------------------------------------------- 1 | ## Scoop for Windows 2 | 3 | ```sh 4 | scoop install lefthook 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/mdbook/installation/snap.md: -------------------------------------------------------------------------------- 1 | ## Snap for Linux 2 | 3 | ```sh 4 | snap install --classic lefthook 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/mdbook/installation/swift.md: -------------------------------------------------------------------------------- 1 | ## Swift 2 | 3 | You can find the Swift wrapper plugin [here](https://github.com/csjones/lefthook-plugin). 4 | 5 | Utilize lefthook in your Swift project using Swift Package Manager: 6 | 7 | ```swift 8 | .package(url: "https://github.com/csjones/lefthook-plugin.git", exact: "1.11.13"), 9 | ``` 10 | 11 | Or, with [mint](https://github.com/yonaskolb/Mint): 12 | 13 | ```bash 14 | mint run csjones/lefthook-plugin 15 | ``` 16 | -------------------------------------------------------------------------------- /docs/mdbook/installation/winget.md: -------------------------------------------------------------------------------- 1 | ## Winget for Windows 2 | 3 | ```sh 4 | winget install evilmartians.lefthook 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/mdbook/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | 5 | 6 | **Lefthook** is a Git hooks manager. Here is how to 7 | 8 | - **[Install](./installation)** lefthook to your project or globally. 9 | 10 | - **[Configure](./configuration)** `lefthook.yml` with detailed options explanation. 11 | 12 | See also: [**examples**](./examples) of lefthook common usage. 13 | 14 | **Example:** Run your linters on `pre-commit` hook and forget about the routine. 15 | 16 | ```yml 17 | # lefthook.yml 18 | 19 | pre-commit: 20 | parallel: true 21 | jobs: 22 | - run: yarn run stylelint --fix {staged_files} 23 | glob: "*.css" 24 | stage_fixed: true 25 | 26 | - run: yarn run eslint --fix "{staged_files}" 27 | glob: 28 | - "*.ts" 29 | - "*.js" 30 | - "*.tsx" 31 | - "*.jsx" 32 | stage_fixed: true 33 | ``` 34 | 35 | --- 36 | 37 | 38 | Sponsored by Evil Martians 39 | 40 | 41 | ❓_If you have a question or found a mistake in the documentation, please create a new [discussion](https://github.com/evilmartians/lefthook/discussions/new/choose). Small contributions help maintaining the quality of the project._ 42 | 43 | -------------------------------------------------------------------------------- /docs/mdbook/misc/contributors.md: -------------------------------------------------------------------------------- 1 | # Contributors 2 | 3 | - [Arkweid](https://github.com/Arkweid) 4 | - [Envek](https://github.com/Envek) 5 | - [mrexox](https://github.com/mrexox) 6 | - [skryukov](https://github.com/skryukov) 7 | - [scop](https://github.com/scop) 8 | - [hyperupcall](https://github.com/hyperupcall) 9 | - [MartijnCuppens](https://github.com/MartijnCuppens) 10 | - [palkan](https://github.com/palkan) 11 | - [markovichecha](https://github.com/markovichecha) 12 | - [technicalpickles](https://github.com/technicalpickles) 13 | - [aminya](https://github.com/aminya) 14 | - [prog-supdex](https://github.com/prog-supdex) 15 | - [HellSquirrel](https://github.com/HellSquirrel) 16 | - [Evilweed](https://github.com/Evilweed) 17 | - [PikachuEXE](https://github.com/PikachuEXE) 18 | - [jsmestad](https://github.com/jsmestad) 19 | - [DmitryTsepelev](https://github.com/DmitryTsepelev) 20 | - [pmirecki](https://github.com/pmirecki) 21 | - [0legovich](https://github.com/0legovich) 22 | - [zachahn](https://github.com/zachahn) 23 | - [sitiom](https://github.com/sitiom) 24 | - [spearmootz](https://github.com/spearmootz) 25 | - [pwinckles](https://github.com/pwinckles) 26 | - [pablobirukov](https://github.com/pablobirukov) 27 | - [nihalgonsalves](https://github.com/nihalgonsalves) 28 | - [nesk](https://github.com/nesk) 29 | - [jaydorsey](https://github.com/jaydorsey) 30 | - [fantua](https://github.com/fantua) 31 | - [orsinium](https://github.com/orsinium) 32 | - [fabn](https://github.com/fabn) 33 | 34 | If you feel you’re missing from this list, feel free to add yourself in a PR. 35 | 36 | 39 | -------------------------------------------------------------------------------- /docs/mdbook/usage/README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | - [Commands](./commands.md) 4 | - [`lefthook install`](./commands.md#lefthook-install) 5 | - [`lefthook uninstall`](./commands.md#lefthook-uninstall) 6 | - [`lefthook add`](./commands.md#lefthook-add) 7 | - [`lefthook run`](./commands.md#lefthook-run) 8 | - [`lefthook version`](./commands.md#lefthook-version) 9 | - [`lefthook self-update`](./commands.md#lefthook-self-update) 10 | - [`lefthook validate`](./commands.md#lefthook-validate) 11 | - [`lefthook dump`](./commands.md#lefthook-dump) 12 | - [ENV variables](./env.md) 13 | - [`LEFTHOOK`](./env.md#lefthook) 14 | - [`LEFTHOOK_EXCLUDE`](./env.md#lefthook_exclude) 15 | - [`LEFTHOOK_OUTPUT`](./env.md#lefthook_output) 16 | - [`LEFTHOOK_QUIET`](./env.md#lefthook_quiet) 17 | - [`LEFTHOOK_VERBOSE`](./env.md#lefthook_verbose) 18 | - [`LEFTHOOK_BIN`](./env.md#lefthook_bin) 19 | - [`NO_COLOR`](./env.md#no_color) 20 | - [`CLICOLOR_FORCE`](./env.md#clicolor_force) 21 | - [Tips](./tips.md) 22 | - [Local config](./tips.md#local-config) 23 | - [Disable lefthook in CI](./tips.md#disable-lefthook-in-ci) 24 | - [Commitlint example](./tips.md#commitlint-example) 25 | - [Parallel execution](./tips.md#parallel-execution) 26 | - [Concurrent files overrides](./tips.md#concurrent-files-overrides) 27 | - [Capture ARGS from git in the script](./tips.md#capture-args-from-git-in-the-script) 28 | - [Git LFS support](./tips.md#git-lfs-support) 29 | - [Pass stdin to a command or script](./tips.md#pass-stdin-to-a-command-or-script) 30 | - [Using an interactive command or script](./tips.md#using-an-interactive-command-or-script) 31 | 32 | -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | > [!IMPORTANT] 4 | > 5 | > This documentation was moved to https://lefthook.dev/usage/ 6 | -------------------------------------------------------------------------------- /examples/commitlint/README.md: -------------------------------------------------------------------------------- 1 | # Use commitlint and/or commitzen 2 | 3 | ## Install dependencies 4 | 5 | ```bash 6 | yarn add -D @commitlint/cli @commitlint/config-conventional 7 | # If using commitzen 8 | yarn add -D commitizen cz-conventional-changelog 9 | ``` 10 | 11 | ## Configure 12 | 13 | Setup `commitlint.config.js`. Conventional configuration: 14 | 15 | ```bash 16 | echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js 17 | ``` 18 | 19 | If you are using commitzen, make sure to add this in `package.json`: 20 | 21 | ```json 22 | "config": { 23 | "commitizen": { 24 | "path": "./node_modules/cz-conventional-changelog" 25 | } 26 | } 27 | ``` 28 | 29 | ## Test it 30 | 31 | ```bash 32 | # You can type it without message, if you are using commitzen 33 | git commit 34 | 35 | # Or provide a commit message is using only commitlint 36 | git commit -am 'fix: typo' 37 | ``` 38 | -------------------------------------------------------------------------------- /examples/commitlint/commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']}; 2 | -------------------------------------------------------------------------------- /examples/commitlint/lefthook.yml: -------------------------------------------------------------------------------- 1 | # Use this to build commit messages 2 | prepare-commit-msg: 3 | commands: 4 | commitzen: 5 | interactive: true 6 | run: yarn run cz --hook # Or npx cz --hook 7 | env: 8 | LEFTHOOK: 0 9 | 10 | # Use this to validate commit messages 11 | commit-msg: 12 | commands: 13 | "lint commit message": 14 | run: yarn run commitlint --edit {1} 15 | -------------------------------------------------------------------------------- /examples/complete/lefthook.yml: -------------------------------------------------------------------------------- 1 | commit-msg: 2 | scripts: 3 | "template_checker": 4 | runner: bash 5 | 6 | pre-commit: 7 | commands: 8 | stylelint: 9 | tags: frontend style 10 | glob: "*.js" 11 | run: yarn stylelint {staged_files} 12 | stage_fixed: true 13 | rubocop: 14 | tags: backend style 15 | glob: "*.rb" 16 | exclude: '(^|/)(application|routes)\.rb$' 17 | run: bundle exec rubocop --force-exclusion {all_files} 18 | stage_fixed: true 19 | scripts: 20 | "good_job.js": 21 | runner: node 22 | 23 | pre-push: 24 | parallel: true 25 | commands: 26 | stylelint: 27 | tags: frontend style 28 | files: git diff --name-only master 29 | glob: "*.js" 30 | run: yarn stylelint {files} 31 | rubocop: 32 | tags: backend style 33 | files: git diff --name-only master 34 | glob: "*.rb" 35 | run: bundle exec rubocop --force-exclusion {files} 36 | scripts: 37 | "verify": 38 | runner: sh 39 | 40 | -------------------------------------------------------------------------------- /examples/remote/ping.yml: -------------------------------------------------------------------------------- 1 | # Test `remotes` config of lefthook. 2 | # 3 | # # lefthook.yml 4 | # 5 | # remotes: 6 | # - git_url: git@github.com:evilmartians/lefthook 7 | # configs: 8 | # - examples/remote/ping.yml 9 | # 10 | # $ lefthook run pre-commit 11 | 12 | pre-commit: 13 | commands: 14 | ping: 15 | run: echo pong 16 | -------------------------------------------------------------------------------- /examples/verbose/lefthook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # lefthook.yml 3 | 4 | # This hook executes on `git commit` 5 | pre-commit: 6 | parallel: true # All commands will be executed concurrently 7 | commands: # Commands section 8 | # `js-lint` will call `npx eslit --fix` only on staged files. 9 | # It will filter staged files by glob. 10 | # If there are no files left after filtering, this command will be skipped 11 | js-lint: 12 | glob: "*.{js,ts}" 13 | run: npx eslint --fix {staged_files} && git add {staged_files} 14 | 15 | # `ruby-test` will skip execution only when in a merging or rebasing state. 16 | ruby-test: 17 | skip: 18 | - merge 19 | - rebase 20 | run: bundle exec rspec 21 | fail_text: Run bundle install 22 | 23 | # `ruby-lint` has `files` option which is a git command for replacing 24 | # the {files} template. Then lefthook applies glob pattern to the result. 25 | # If the final list is empty, the command will be skipped. 26 | # Otherwise the {files} templace will be replaces with list. 27 | # 28 | # Note: if a template has surrounding quotes, they will be used to wrap 29 | # each file in the list. 30 | # Double quotes `"` and single quotes `'` are supported. 31 | ruby-lint: 32 | glob: "*.rb" 33 | files: git diff-tree -r --name-only --diff-filter=CDMR HEAD origin/master 34 | run: bundle exec rubocop --force-exclusion --parallel '{files}' 35 | 36 | # You can provide more hooks. 37 | pre-push: 38 | commands: 39 | spelling: 40 | files: git diff --name-only HEAD @{push} 41 | glob: "*.md" 42 | run: npx yaspeller {files} 43 | -------------------------------------------------------------------------------- /examples/with_scripts/lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | scripts: 3 | "good_job.js": 4 | runner: node 5 | -------------------------------------------------------------------------------- /integrity_test.go: -------------------------------------------------------------------------------- 1 | //go:build integrity 2 | // +build integrity 3 | 4 | package main_test 5 | 6 | import ( 7 | "fmt" 8 | "os" 9 | "testing" 10 | 11 | "github.com/rogpeppe/go-internal/testscript" 12 | ) 13 | 14 | func TestLefthookIntegrity(t *testing.T) { 15 | testscript.Run(t, testscript.Params{ 16 | Dir: "testdata", 17 | Setup: func(env *testscript.Env) error { 18 | env.Vars = append(env.Vars, fmt.Sprintf("GOCOVERDIR=%s", os.Getenv("GOCOVERDIR"))) 19 | return nil 20 | }, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /internal/config/available_hooks.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | // ChecksumFileName - the file, which is used just to store the current config checksum version. 4 | const ChecksumFileName = "lefthook.checksum" 5 | 6 | // GhostHookName - the hook which logs are not shown and which is used for synchronizing hooks. 7 | const GhostHookName = "prepare-commit-msg" 8 | 9 | // AvailableHooks - list of hooks taken from https://git-scm.com/docs/githooks. 10 | // Keep the order of the hooks same here for easy syncing. 11 | var AvailableHooks = map[string]struct{}{ 12 | "applypatch-msg": {}, 13 | "pre-applypatch": {}, 14 | "post-applypatch": {}, 15 | "pre-commit": {}, 16 | "pre-merge-commit": {}, 17 | "prepare-commit-msg": {}, 18 | "commit-msg": {}, 19 | "post-commit": {}, 20 | "pre-rebase": {}, 21 | "post-checkout": {}, 22 | "post-merge": {}, 23 | "pre-push": {}, 24 | "pre-receive": {}, 25 | "update": {}, 26 | "proc-receive": {}, 27 | "post-receive": {}, 28 | "post-update": {}, 29 | "reference-transaction": {}, 30 | "push-to-checkout": {}, 31 | "pre-auto-gc": {}, 32 | "post-rewrite": {}, 33 | "sendemail-validate": {}, 34 | "fsmonitor-watchman": {}, 35 | "p4-changelist": {}, 36 | "p4-prepare-changelist": {}, 37 | "p4-post-changelist": {}, 38 | "p4-pre-submit": {}, 39 | "post-index-change": {}, 40 | } 41 | 42 | func HookUsesStagedFiles(hook string) bool { 43 | return hook == "pre-commit" 44 | } 45 | 46 | func HookUsesPushFiles(hook string) bool { 47 | return hook == "pre-push" 48 | } 49 | 50 | func KnownHook(hook string) bool { 51 | _, ok := AvailableHooks[hook] 52 | return ok 53 | } 54 | -------------------------------------------------------------------------------- /internal/config/command.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ErrFilesIncompatible = errors.New("one of your runners contains incompatible file types") 8 | 9 | type Command struct { 10 | Run string `json:"run" mapstructure:"run" toml:"run" yaml:"run"` 11 | Files string `json:"files,omitempty" mapstructure:"files" toml:"files,omitempty" yaml:",omitempty"` 12 | 13 | Skip interface{} `json:"skip,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"` 14 | Only interface{} `json:"only,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"` 15 | Tags []string `json:"tags,omitempty" jsonschema:"oneof_type=string;array" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"` 16 | Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"` 17 | 18 | FileTypes []string `json:"file_types,omitempty" koanf:"file_types" mapstructure:"file_types" toml:"file_types,omitempty" yaml:"file_types,omitempty"` 19 | 20 | Glob []string `json:"glob,omitempty" jsonschema:"oneof_type=string;array" mapstructure:"glob" toml:"glob,omitempty" yaml:",omitempty"` 21 | Root string `json:"root,omitempty" mapstructure:"root" toml:"root,omitempty" yaml:",omitempty"` 22 | Exclude interface{} `json:"exclude,omitempty" jsonschema:"oneof_type=string;array" mapstructure:"exclude" toml:"exclude,omitempty" yaml:",omitempty"` 23 | 24 | Priority int `json:"priority,omitempty" mapstructure:"priority" toml:"priority,omitempty" yaml:",omitempty"` 25 | FailText string `json:"fail_text,omitempty" koanf:"fail_text" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"` 26 | Interactive bool `json:"interactive,omitempty" mapstructure:"interactive" toml:"interactive,omitempty" yaml:",omitempty"` 27 | UseStdin bool `json:"use_stdin,omitempty" koanf:"use_stdin" mapstructure:"use_stdin" toml:"use_stdin,omitempty" yaml:"use_stdin,omitempty"` 28 | StageFixed bool `json:"stage_fixed,omitempty" koanf:"stage_fixed" mapstructure:"stage_fixed" toml:"stage_fixed,omitempty" yaml:"stage_fixed,omitempty"` 29 | } 30 | 31 | func (c Command) ExecutionPriority() int { 32 | return c.Priority 33 | } 34 | -------------------------------------------------------------------------------- /internal/config/command_executor.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "runtime" 6 | "strings" 7 | 8 | "github.com/evilmartians/lefthook/internal/log" 9 | "github.com/evilmartians/lefthook/internal/system" 10 | ) 11 | 12 | // commandExecutor implements execution of a skip checks passed in a `run` option. 13 | type commandExecutor struct { 14 | cmd system.Command 15 | } 16 | 17 | // cmd runs plain string command in a subshell returning the success of it. 18 | func (c *commandExecutor) execute(commandLine string) bool { 19 | if commandLine == "" { 20 | return false 21 | } 22 | 23 | var args []string 24 | if runtime.GOOS == "windows" { 25 | args = []string{"powershell", "-Command", commandLine} 26 | } else { 27 | args = []string{"sh", "-c", commandLine} 28 | } 29 | 30 | stdout := new(bytes.Buffer) 31 | stderr := new(bytes.Buffer) 32 | 33 | err := c.cmd.Run(args, "", system.NullReader, stdout, stderr) 34 | 35 | b := log.Builder(log.DebugLevel, "[lefthook] "). 36 | Add("run: ", strings.Join(args, " ")). 37 | Add("out: ", stdout.String()). 38 | Add("err: ", stderr.String()) 39 | 40 | if err != nil { 41 | b.Add("!: ", err.Error()) 42 | } 43 | 44 | b.Log() 45 | 46 | return err == nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/config/files.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "strings" 4 | 5 | const ( 6 | SubFiles string = "{files}" 7 | SubAllFiles string = "{all_files}" 8 | SubStagedFiles string = "{staged_files}" 9 | SubPushFiles string = "{push_files}" 10 | ) 11 | 12 | func IsRunFilesCompatible(run string) bool { 13 | return !strings.Contains(run, SubStagedFiles) || !strings.Contains(run, SubPushFiles) 14 | } 15 | -------------------------------------------------------------------------------- /internal/config/hook.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/evilmartians/lefthook/internal/git" 5 | "github.com/evilmartians/lefthook/internal/system" 6 | ) 7 | 8 | const CMD = "{cmd}" 9 | 10 | type Hook struct { 11 | Parallel bool `json:"parallel,omitempty" mapstructure:"parallel" toml:"parallel,omitempty" yaml:",omitempty"` 12 | Piped bool `json:"piped,omitempty" mapstructure:"piped" toml:"piped,omitempty" yaml:",omitempty"` 13 | Follow bool `json:"follow,omitempty" mapstructure:"follow" toml:"follow,omitempty" yaml:",omitempty"` 14 | Files string `json:"files,omitempty" mapstructure:"files" toml:"files,omitempty" yaml:",omitempty"` 15 | ExcludeTags []string `json:"exclude_tags,omitempty" koanf:"exclude_tags" mapstructure:"exclude_tags" toml:"exclude_tags,omitempty" yaml:"exclude_tags,omitempty"` 16 | Skip interface{} `json:"skip,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"` 17 | Only interface{} `json:"only,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"` 18 | 19 | Jobs []*Job `json:"jobs,omitempty" mapstructure:"jobs" toml:"jobs,omitempty" yaml:",omitempty"` 20 | 21 | Commands map[string]*Command `json:"commands,omitempty" mapstructure:"-" toml:"commands,omitempty" yaml:",omitempty"` 22 | Scripts map[string]*Script `json:"scripts,omitempty" mapstructure:"-" toml:"scripts,omitempty" yaml:",omitempty"` 23 | } 24 | 25 | func (h *Hook) DoSkip(state func() git.State) bool { 26 | skipChecker := NewSkipChecker(system.Cmd) 27 | return skipChecker.Check(state, h.Skip, h.Only) 28 | } 29 | -------------------------------------------------------------------------------- /internal/config/remote.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Remote struct { 4 | GitURL string `json:"git_url,omitempty" jsonschema:"description=A URL to Git repository. It will be accessed with privileges of the machine lefthook runs on." koanf:"git_url" mapstructure:"git_url" toml:"git_url" yaml:"git_url"` 5 | 6 | Ref string `json:"ref,omitempty" jsonschema:"description=An optional *branch* or *tag* name" mapstructure:"ref,omitempty" toml:"ref,omitempty" yaml:",omitempty"` 7 | 8 | Configs []string `json:"configs,omitempty" jsonschema:"description=An optional array of config paths from remote's root,default=lefthook.yml" mapstructure:"configs,omitempty" toml:"configs,omitempty" yaml:",omitempty"` 9 | 10 | Refetch bool `json:"refetch,omitempty" jsonschema:"description=Set to true if you want to always refetch the remote" mapstructure:"refetch,omitempty" toml:"refetch,omitempty" yaml:",omitempty"` 11 | 12 | RefetchFrequency string `json:"refetch_frequency,omitempty" jsonschema:"description=Provide a frequency for the remotes refetches,example=24h" koanf:"refetch_frequency" mapstructure:"refetch_frequency,omitempty" toml:"refetch_frequency,omitempty" yaml:",omitempty"` 13 | 14 | // Deprecated: use `configs` 15 | Config string `json:"config,omitempty" jsonschema:"description=Deprecated: use configs" mapstructure:"config,omitempty" toml:"config,omitempty" yaml:",omitempty"` 16 | } 17 | 18 | func (r *Remote) Configured() bool { 19 | if r == nil { 20 | return false 21 | } 22 | 23 | return len(r.GitURL) > 0 24 | } 25 | -------------------------------------------------------------------------------- /internal/config/script.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/evilmartians/lefthook/internal/git" 5 | "github.com/evilmartians/lefthook/internal/system" 6 | ) 7 | 8 | type Script struct { 9 | Runner string `json:"runner,omitempty" mapstructure:"runner" toml:"runner,omitempty" yaml:"runner,omitempty"` 10 | 11 | Skip interface{} `json:"skip,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"` 12 | Only interface{} `json:"only,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"` 13 | Tags []string `json:"tags,omitempty" jsonschema:"oneof_type=string;array" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"` 14 | Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"` 15 | Priority int `json:"priority,omitempty" mapstructure:"priority" toml:"priority,omitempty" yaml:",omitempty"` 16 | 17 | FailText string `json:"fail_text,omitempty" koanf:"fail_text" mapstructure:"fail_text" toml:"fail_text,omitempty" yaml:"fail_text,omitempty"` 18 | Interactive bool `json:"interactive,omitempty" mapstructure:"interactive" toml:"interactive,omitempty" yaml:",omitempty"` 19 | UseStdin bool `json:"use_stdin,omitempty" koanf:"use_stdin" mapstructure:"use_stdin" toml:"use_stdin,omitempty" yaml:"use_stdin,omitempty"` 20 | StageFixed bool `json:"stage_fixed,omitempty" koanf:"stage_fixed" mapstructure:"stage_fixed" toml:"stage_fixed,omitempty" yaml:"stage_fixed,omitempty"` 21 | } 22 | 23 | func (s Script) DoSkip(state func() git.State) bool { 24 | skipChecker := NewSkipChecker(system.Cmd) 25 | return skipChecker.Check(state, s.Skip, s.Only) 26 | } 27 | 28 | func (s Script) ExecutionPriority() int { 29 | return s.Priority 30 | } 31 | -------------------------------------------------------------------------------- /internal/config/skip_checker.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/gobwas/glob" 5 | 6 | "github.com/evilmartians/lefthook/internal/git" 7 | "github.com/evilmartians/lefthook/internal/log" 8 | "github.com/evilmartians/lefthook/internal/system" 9 | ) 10 | 11 | type skipChecker struct { 12 | exec *commandExecutor 13 | } 14 | 15 | func NewSkipChecker(cmd system.Command) *skipChecker { 16 | return &skipChecker{&commandExecutor{cmd}} 17 | } 18 | 19 | // check returns the result of applying a skip/only setting which can be a branch, git state, shell command, etc. 20 | func (sc *skipChecker) Check(state func() git.State, skip interface{}, only interface{}) bool { 21 | if skip == nil && only == nil { 22 | return false 23 | } 24 | 25 | if skip != nil { 26 | if sc.matches(state, skip) { 27 | return true 28 | } 29 | } 30 | 31 | if only != nil { 32 | return !sc.matches(state, only) 33 | } 34 | 35 | return false 36 | } 37 | 38 | func (sc *skipChecker) matches(state func() git.State, value interface{}) bool { 39 | switch typedValue := value.(type) { 40 | case bool: 41 | return typedValue 42 | case string: 43 | return typedValue == state().State 44 | case []interface{}: 45 | return sc.matchesSlices(state, typedValue) 46 | } 47 | return false 48 | } 49 | 50 | func (sc *skipChecker) matchesSlices(gitState func() git.State, slice []interface{}) bool { 51 | for _, state := range slice { 52 | switch typedState := state.(type) { 53 | case string: 54 | if typedState == gitState().State { 55 | return true 56 | } 57 | case map[string]interface{}: 58 | if sc.matchesRef(gitState, typedState) { 59 | return true 60 | } 61 | 62 | if sc.matchesCommands(typedState) { 63 | return true 64 | } 65 | } 66 | } 67 | 68 | return false 69 | } 70 | 71 | func (sc *skipChecker) matchesRef(state func() git.State, typedState map[string]interface{}) bool { 72 | ref, ok := typedState["ref"].(string) 73 | if !ok { 74 | return false 75 | } 76 | 77 | branch := state().Branch 78 | if ref == branch { 79 | return true 80 | } 81 | 82 | g := glob.MustCompile(ref) 83 | 84 | return g.Match(branch) 85 | } 86 | 87 | func (sc *skipChecker) matchesCommands(typedState map[string]interface{}) bool { 88 | commandLine, ok := typedState["run"].(string) 89 | if !ok { 90 | return false 91 | } 92 | 93 | result := sc.exec.execute(commandLine) 94 | 95 | log.Builder(log.DebugLevel, "[lefthook] "). 96 | Add("skip/only: ", commandLine). 97 | Add("result: ", result). 98 | Log() 99 | 100 | return result 101 | } 102 | -------------------------------------------------------------------------------- /internal/git/lfs.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "os/exec" 5 | ) 6 | 7 | const ( 8 | LFSRequiredFile = ".lfs-required" 9 | LFSConfigFile = ".lfsconfig" 10 | ) 11 | 12 | var lfsHooks = map[string]struct{}{ 13 | "post-checkout": {}, 14 | "post-commit": {}, 15 | "post-merge": {}, 16 | "pre-push": {}, 17 | } 18 | 19 | // IsLFSAvailable returns 'true' if git-lfs is installed. 20 | func IsLFSAvailable() bool { 21 | _, err := exec.LookPath("git-lfs") 22 | 23 | return err == nil 24 | } 25 | 26 | // IsLFSHook returns whether the hookName is supported by Git LFS. 27 | func IsLFSHook(hookName string) bool { 28 | _, ok := lfsHooks[hookName] 29 | return ok 30 | } 31 | -------------------------------------------------------------------------------- /internal/git/repository_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/evilmartians/lefthook/internal/system" 11 | ) 12 | 13 | type gitCmd struct { 14 | cases map[string]string 15 | } 16 | 17 | func (g gitCmd) WithoutEnvs(...string) system.Command { 18 | return g 19 | } 20 | 21 | func (g gitCmd) Run(cmd []string, _root string, _in io.Reader, out io.Writer, _errOut io.Writer) error { 22 | res, ok := g.cases[(strings.Join(cmd, " "))] 23 | if !ok { 24 | return errors.New("doesn't exist") 25 | } 26 | 27 | _, err := out.Write([]byte(strings.TrimSpace(res))) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return nil 33 | } 34 | 35 | func TestPartiallyStagedFiles(t *testing.T) { 36 | for i, tt := range [...]struct { 37 | name, gitOut string 38 | error bool 39 | result []string 40 | }{ 41 | { 42 | gitOut: `RM old-file -> new file 43 | M staged 44 | MM staged but changed 45 | `, 46 | result: []string{"new file", "staged but changed"}, 47 | }, 48 | } { 49 | t.Run(fmt.Sprintf("%d: %s", i, tt.name), func(t *testing.T) { 50 | repository := &Repository{ 51 | Git: &CommandExecutor{ 52 | cmd: gitCmd{ 53 | cases: map[string]string{ 54 | "git status --short --porcelain": tt.gitOut, 55 | }, 56 | }, 57 | }, 58 | } 59 | repository.Setup() 60 | 61 | files, err := repository.PartiallyStagedFiles() 62 | if tt.error && err != nil { 63 | t.Errorf("expected an error") 64 | } 65 | 66 | if len(files) != len(tt.result) { 67 | t.Errorf("expected %d files, but %d returned", len(tt.result), len(files)) 68 | } 69 | 70 | for j, file := range files { 71 | if tt.result[j] != file { 72 | t.Errorf("file at index %d don't match: %s - %s", j, tt.result[j], file) 73 | } 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/git/state.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | "path/filepath" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/evilmartians/lefthook/internal/log" 11 | ) 12 | 13 | type State struct { 14 | Branch, State string 15 | } 16 | 17 | const ( 18 | Nil string = "" 19 | Merge string = "merge" 20 | MergeCommit string = "merge-commit" 21 | Rebase string = "rebase" 22 | ) 23 | 24 | var ( 25 | refBranchRegexp = regexp.MustCompile(`^ref:\s*refs/heads/(.+)$`) 26 | cmdParentCommits = []string{"git", "show", "--no-patch", `--format="%P"`} 27 | ) 28 | 29 | var ( 30 | state State 31 | stateInitialized bool 32 | ) 33 | 34 | func ResetState() { 35 | stateInitialized = false 36 | } 37 | 38 | func (r *Repository) State() State { 39 | if stateInitialized { 40 | return state 41 | } 42 | 43 | stateInitialized = true 44 | branch := r.Branch() 45 | if r.inMergeState() { 46 | state = State{ 47 | Branch: branch, 48 | State: Merge, 49 | } 50 | return state 51 | } 52 | if r.inRebaseState() { 53 | state = State{ 54 | Branch: branch, 55 | State: Rebase, 56 | } 57 | return state 58 | } 59 | if r.inMergeCommitState() { 60 | state = State{ 61 | Branch: branch, 62 | State: MergeCommit, 63 | } 64 | return state 65 | } 66 | 67 | state = State{ 68 | Branch: branch, 69 | State: Nil, 70 | } 71 | return state 72 | } 73 | 74 | func (r *Repository) Branch() string { 75 | headFile := filepath.Join(r.GitPath, "HEAD") 76 | if _, err := r.Fs.Stat(headFile); os.IsNotExist(err) { 77 | return "" 78 | } 79 | 80 | file, err := r.Fs.Open(headFile) 81 | if err != nil { 82 | return "" 83 | } 84 | defer func() { 85 | if cErr := file.Close(); cErr != nil { 86 | log.Warnf("Could not close %s: %s", headFile, cErr) 87 | } 88 | }() 89 | 90 | scanner := bufio.NewScanner(file) 91 | scanner.Split(bufio.ScanLines) 92 | 93 | for scanner.Scan() { 94 | match := refBranchRegexp.FindStringSubmatch(scanner.Text()) 95 | 96 | if len(match) > 1 { 97 | return match[1] 98 | } 99 | } 100 | 101 | return "" 102 | } 103 | 104 | func (r *Repository) inMergeState() bool { 105 | if _, err := r.Fs.Stat(filepath.Join(r.GitPath, "MERGE_HEAD")); os.IsNotExist(err) { 106 | return false 107 | } 108 | return true 109 | } 110 | 111 | func (r *Repository) inRebaseState() bool { 112 | if _, mergeErr := r.Fs.Stat(filepath.Join(r.GitPath, "rebase-merge")); os.IsNotExist(mergeErr) { 113 | if _, applyErr := r.Fs.Stat(filepath.Join(r.GitPath, "rebase-apply")); os.IsNotExist(applyErr) { 114 | return false 115 | } 116 | } 117 | 118 | return true 119 | } 120 | 121 | func (r *Repository) inMergeCommitState() bool { 122 | parents, err := r.Git.Cmd(cmdParentCommits) 123 | if err != nil { 124 | return false 125 | } 126 | 127 | return strings.Contains(parents, " ") 128 | } 129 | -------------------------------------------------------------------------------- /internal/lefthook/add.go: -------------------------------------------------------------------------------- 1 | package lefthook 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/evilmartians/lefthook/internal/config" 8 | "github.com/evilmartians/lefthook/internal/templates" 9 | ) 10 | 11 | const defaultDirMode = 0o755 12 | 13 | type AddArgs struct { 14 | Hook string 15 | 16 | CreateDirs, Force bool 17 | } 18 | 19 | func Add(opts *Options, args *AddArgs) error { 20 | lefthook, err := initialize(opts) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return lefthook.Add(args) 26 | } 27 | 28 | // Creates a hook, given in args. The hook is a Lefthook hook. 29 | func (l *Lefthook) Add(args *AddArgs) error { 30 | if !config.KnownHook(args.Hook) { 31 | return fmt.Errorf("skip adding, hook is unavailable: %s", args.Hook) 32 | } 33 | 34 | err := l.cleanHook(args.Hook, args.Force || l.Force) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | if err = l.ensureHooksDirExists(); err != nil { 40 | return err 41 | } 42 | 43 | err = l.addHook(args.Hook, templates.Args{}) 44 | if err != nil { 45 | return err 46 | } 47 | 48 | if args.CreateDirs { 49 | global, local := l.getSourceDirs() 50 | 51 | sourceDir := filepath.Join(l.repo.RootPath, global, args.Hook) 52 | sourceDirLocal := filepath.Join(l.repo.RootPath, local, args.Hook) 53 | 54 | if err = l.Fs.MkdirAll(sourceDir, defaultDirMode); err != nil { 55 | return err 56 | } 57 | if err = l.Fs.MkdirAll(sourceDirLocal, defaultDirMode); err != nil { 58 | return err 59 | } 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (l *Lefthook) getSourceDirs() (global, local string) { 66 | global = config.DefaultSourceDir 67 | local = config.DefaultSourceDirLocal 68 | 69 | cfg, err := config.Load(l.Fs, l.repo) 70 | if err == nil { 71 | if len(cfg.SourceDir) > 0 { 72 | global = cfg.SourceDir 73 | } 74 | if len(cfg.SourceDirLocal) > 0 { 75 | local = cfg.SourceDirLocal 76 | } 77 | } 78 | 79 | return 80 | } 81 | -------------------------------------------------------------------------------- /internal/lefthook/dump.go: -------------------------------------------------------------------------------- 1 | package lefthook 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/evilmartians/lefthook/internal/config" 7 | "github.com/evilmartians/lefthook/internal/log" 8 | ) 9 | 10 | type DumpArgs struct { 11 | JSON bool 12 | TOML bool 13 | Format string 14 | } 15 | 16 | func Dump(opts *Options, args DumpArgs) { 17 | lefthook, err := initialize(opts) 18 | if err != nil { 19 | log.Errorf("couldn't initialize lefthook: %s\n", err) 20 | return 21 | } 22 | 23 | cfg, err := config.Load(lefthook.Fs, lefthook.repo) 24 | if err != nil { 25 | log.Errorf("couldn't load config: %s\n", err) 26 | return 27 | } 28 | 29 | var format config.DumpFormat 30 | 31 | switch args.Format { 32 | case "yaml": 33 | format = config.YAMLFormat 34 | case "json": 35 | format = config.JSONFormat 36 | case "toml": 37 | format = config.TOMLFormat 38 | default: 39 | format = config.YAMLFormat 40 | } 41 | 42 | if args.JSON { 43 | format = config.JSONFormat 44 | } 45 | 46 | if args.TOML { 47 | format = config.TOMLFormat 48 | } 49 | 50 | if err := cfg.Dump(format, os.Stdout); err != nil { 51 | log.Errorf("couldn't dump config: %s\n", err) 52 | return 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/lefthook/runner/cached_reader.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | // cachedReader reads from the provided `io.Reader` until `io.EOF` and saves 9 | // the read content into the inner buffer. 10 | // 11 | // After `io.EOF` it will be providing the read data again and again. 12 | type cachedReader struct { 13 | in io.Reader 14 | useBuffer bool 15 | buf []byte 16 | reader *bytes.Reader 17 | } 18 | 19 | func NewCachedReader(in io.Reader) *cachedReader { 20 | return &cachedReader{ 21 | in: in, 22 | buf: []byte{}, 23 | reader: bytes.NewReader([]byte{}), 24 | } 25 | } 26 | 27 | func (r *cachedReader) Read(p []byte) (int, error) { 28 | if r.useBuffer { 29 | n, err := r.reader.Read(p) 30 | if err == io.EOF { 31 | _, seekErr := r.reader.Seek(0, io.SeekStart) 32 | if seekErr != nil { 33 | panic(seekErr) 34 | } 35 | 36 | return n, err 37 | } 38 | 39 | return n, err 40 | } 41 | 42 | n, err := r.in.Read(p) 43 | r.buf = append(r.buf, p[:n]...) 44 | if err == io.EOF { 45 | r.useBuffer = true 46 | r.reader = bytes.NewReader(r.buf) 47 | } 48 | return n, err 49 | } 50 | -------------------------------------------------------------------------------- /internal/lefthook/runner/cached_reader_test.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | ) 8 | 9 | func TestCachedReader(t *testing.T) { 10 | testSlice := []byte("Some example string\nMultiline") 11 | 12 | cachedReader := NewCachedReader(bytes.NewReader(testSlice)) 13 | 14 | for range 5 { 15 | res, err := io.ReadAll(cachedReader) 16 | if err != nil { 17 | t.Errorf("unexpected err: %s", err) 18 | } 19 | 20 | if !bytes.Equal(res, testSlice) { 21 | t.Errorf("expected %v to be equal to %v", res, testSlice) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/lefthook/runner/exec/executor.go: -------------------------------------------------------------------------------- 1 | package exec 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // Options contains the data that controls the execution. 9 | type Options struct { 10 | Name, Root string 11 | Commands []string 12 | Env map[string]string 13 | Interactive, UseStdin bool 14 | } 15 | 16 | // Executor provides an interface for command execution. 17 | // It is used here for testing purpose mostly. 18 | type Executor interface { 19 | Execute(context.Context, Options, io.Reader, io.Writer) error 20 | } 21 | -------------------------------------------------------------------------------- /internal/lefthook/runner/filters/detect_text.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import ( 4 | "bytes" 5 | ) 6 | 7 | // See: https://github.com/gabriel-vasile/mimetype/blob/6e3aeb1/internal/charset/charset.go 8 | 9 | var boms = [][]byte{ 10 | {0xEF, 0xBB, 0xBF}, // utf-8 11 | {0x00, 0x00, 0xFE, 0xFF}, // utf-32be 12 | {0xFF, 0xFE, 0x00, 0x00}, // utf-32le 13 | {0xFE, 0xFF}, // utf-16be 14 | {0xFF, 0xFE}, // utf-16le 15 | } 16 | 17 | // hasBOM returns true if the charset declared in the BOM of content. 18 | func hasBOM(content []byte) bool { 19 | for _, bom := range boms { 20 | if bytes.HasPrefix(content, bom) { 21 | return true 22 | } 23 | } 24 | return false 25 | } 26 | 27 | // detectText checks if a sequence contains of a plain text bytes. 28 | // 29 | // This function does not parse BOM-less UTF16 and UTF32 files. Not really 30 | // sure it should. Linux file utility also requires a BOM for UTF16 and UTF32. 31 | func detectText(bytes []byte) bool { 32 | if hasBOM(bytes) { 33 | return true 34 | } 35 | 36 | // Binary data bytes as defined here: https://mimesniff.spec.whatwg.org/#binary-data-byte 37 | for _, b := range bytes { 38 | if b <= 0x08 || 39 | b == 0x0B || 40 | 0x0E <= b && b <= 0x1A || 41 | 0x1C <= b && b <= 0x1F { 42 | return false 43 | } 44 | } 45 | 46 | return true 47 | } 48 | -------------------------------------------------------------------------------- /internal/lefthook/runner/filters/detect_text_test.go: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestDetectText(t *testing.T) { 9 | for i, tt := range [...]struct { 10 | bytes []byte 11 | result bool 12 | }{ 13 | { 14 | bytes: []byte{}, 15 | result: true, 16 | }, 17 | { 18 | bytes: []byte{0xEF, 0xBB, 0xBF}, // utf-8 BOM 19 | result: true, 20 | }, 21 | { 22 | bytes: []byte{0x00, 0x00, 0xFE, 0xFF}, // utf-32be BOM 23 | result: true, 24 | }, 25 | { 26 | bytes: []byte{0xFF, 0xFE, 0x00, 0x00}, // utf-32le BOM 27 | result: true, 28 | }, 29 | { 30 | bytes: []byte{0xFE, 0xFF}, // utf-16be BOM 31 | result: true, 32 | }, 33 | { 34 | bytes: []byte{0xFF, 0xFE}, // utf-16le BOM 35 | result: true, 36 | }, 37 | { 38 | bytes: []byte{0xFA, 0xCF, 0xFE, 0xED, 0x00, 0x0C}, 39 | result: false, 40 | }, 41 | { 42 | bytes: []byte{0x70, 0x5B, 0x65, 0x72, 0x63, 0x2D}, // .lefthook.toml 43 | result: true, 44 | }, 45 | { 46 | bytes: []byte{0x5B, 0x21, 0x75, 0x42, 0x6C, 0x69, 0x20, 0x64, 0x74, 0x53, 0x74, 0x61, 0x73, 0x75, 0x28, 0x5D}, // README.md 47 | result: true, 48 | }, 49 | } { 50 | t.Run(fmt.Sprintf("#%d:", i), func(t *testing.T) { 51 | if detectText(tt.bytes) != tt.result { 52 | t.Error("results don't match; expected", tt.result) 53 | } 54 | }) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/lefthook/runner/jobs/build_script.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/alessio/shellescape" 10 | 11 | "github.com/evilmartians/lefthook/internal/log" 12 | ) 13 | 14 | const ( 15 | executableFileMode os.FileMode = 0o751 16 | executableMask os.FileMode = 0o111 17 | ) 18 | 19 | type scriptNotExistsError struct { 20 | scriptPath string 21 | } 22 | 23 | func (s scriptNotExistsError) Error() string { 24 | return fmt.Sprintf("script does not exist: %s", s.scriptPath) 25 | } 26 | 27 | func buildScript(params *Params) (*Job, error) { 28 | if err := params.validateScript(); err != nil { 29 | return nil, err 30 | } 31 | 32 | var scriptExists bool 33 | execs := make([]string, 0) 34 | for _, sourceDir := range params.SourceDirs { 35 | scriptPath := filepath.Join(sourceDir, params.HookName, params.Script) 36 | fileInfo, err := params.Repo.Fs.Stat(scriptPath) 37 | if os.IsNotExist(err) { 38 | log.Debugf("[lefthook] script doesn't exist: %s", scriptPath) 39 | continue 40 | } 41 | if err != nil { 42 | log.Errorf("Failed to get info about a script: %s", params.Script) 43 | return nil, err 44 | } 45 | 46 | scriptExists = true 47 | 48 | if !fileInfo.Mode().IsRegular() { 49 | log.Debugf("[lefthook] script '%s' is not a regular file, skipping", scriptPath) 50 | return nil, &SkipError{"not a regular file"} 51 | } 52 | 53 | // Make sure file is executable 54 | if (fileInfo.Mode() & executableMask) == 0 { 55 | if err := params.Repo.Fs.Chmod(scriptPath, executableFileMode); err != nil { 56 | log.Errorf("Couldn't change file mode to make file executable: %s", err) 57 | return nil, err 58 | } 59 | } 60 | 61 | var args []string 62 | if len(params.Runner) > 0 { 63 | args = append(args, params.Runner) 64 | } 65 | 66 | args = append(args, shellescape.Quote(scriptPath)) 67 | args = append(args, params.GitArgs...) 68 | 69 | execs = append(execs, strings.Join(args, " ")) 70 | } 71 | 72 | if !scriptExists { 73 | return nil, scriptNotExistsError{params.Script} 74 | } 75 | 76 | return &Job{ 77 | Execs: execs, 78 | Files: []string{}, 79 | }, nil 80 | } 81 | -------------------------------------------------------------------------------- /internal/lefthook/runner/jobs/jobs.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | import ( 4 | "github.com/evilmartians/lefthook/internal/config" 5 | "github.com/evilmartians/lefthook/internal/git" 6 | "github.com/evilmartians/lefthook/internal/system" 7 | ) 8 | 9 | type Params struct { 10 | Repo *git.Repository 11 | Hook *config.Hook 12 | HookName string 13 | GitArgs []string 14 | Force bool 15 | ForceFiles []string 16 | SourceDirs []string 17 | 18 | Run string 19 | Root string 20 | Runner string 21 | Script string 22 | Files string 23 | FileTypes []string 24 | Tags []string 25 | Glob []string 26 | Templates map[string]string 27 | Exclude interface{} 28 | Only interface{} 29 | Skip interface{} 30 | } 31 | 32 | type Job struct { 33 | Execs []string 34 | Files []string 35 | } 36 | 37 | func New(name string, params *Params) (*Job, error) { 38 | if params.skip() { 39 | return nil, SkipError{"by condition"} 40 | } 41 | 42 | if intersect(params.Hook.ExcludeTags, params.Tags) { 43 | return nil, SkipError{"tags"} 44 | } 45 | 46 | if intersect(params.Hook.ExcludeTags, []string{name}) { 47 | return nil, SkipError{"name"} 48 | } 49 | 50 | var err error 51 | var job *Job 52 | if len(params.Run) != 0 { 53 | job, err = buildCommand(params) 54 | } else { 55 | job, err = buildScript(params) 56 | } 57 | 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return job, nil 63 | } 64 | 65 | func (p *Params) skip() bool { 66 | skipChecker := config.NewSkipChecker(system.Cmd) 67 | return skipChecker.Check(p.Repo.State, p.Skip, p.Only) 68 | } 69 | 70 | func (p *Params) validateCommand() error { 71 | if !config.IsRunFilesCompatible(p.Run) { 72 | return config.ErrFilesIncompatible 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (p *Params) validateScript() error { 79 | return nil 80 | } 81 | 82 | func intersect(a, b []string) bool { 83 | intersections := make(map[string]struct{}, len(a)) 84 | 85 | for _, v := range a { 86 | intersections[v] = struct{}{} 87 | } 88 | 89 | for _, v := range b { 90 | if _, ok := intersections[v]; ok { 91 | return true 92 | } 93 | } 94 | 95 | return false 96 | } 97 | -------------------------------------------------------------------------------- /internal/lefthook/runner/jobs/skip_error.go: -------------------------------------------------------------------------------- 1 | package jobs 2 | 3 | // SkipError implements error interface but indicates that the execution needs to be skipped. 4 | type SkipError struct { 5 | reason string 6 | } 7 | 8 | func (r SkipError) Error() string { 9 | return r.reason 10 | } 11 | -------------------------------------------------------------------------------- /internal/lefthook/runner/result.go: -------------------------------------------------------------------------------- 1 | package runner 2 | 3 | type status int8 4 | 5 | const ( 6 | success status = iota 7 | failure 8 | skip 9 | ) 10 | 11 | // Result contains name of a command/script and an optional fail string. 12 | type Result struct { 13 | Sub []Result 14 | Name string 15 | text string 16 | status status 17 | } 18 | 19 | func (r Result) Success() bool { 20 | return r.status == success 21 | } 22 | 23 | func (r Result) Failure() bool { 24 | return r.status == failure 25 | } 26 | 27 | func (r Result) Text() string { 28 | return r.text 29 | } 30 | 31 | func skipped(name string) Result { 32 | return Result{Name: name, status: skip} 33 | } 34 | 35 | func succeeded(name string) Result { 36 | return Result{Name: name, status: success} 37 | } 38 | 39 | func failed(name, text string) Result { 40 | return Result{Name: name, status: failure, text: text} 41 | } 42 | 43 | func groupResult(name string, results []Result) Result { 44 | stat := success 45 | for _, res := range results { 46 | if res.status == failure { 47 | stat = failure 48 | break 49 | } 50 | if res.status == skip { 51 | stat = skip 52 | } 53 | } 54 | 55 | return Result{Name: name, status: stat, Sub: results} 56 | } 57 | -------------------------------------------------------------------------------- /internal/lefthook/uninstall.go: -------------------------------------------------------------------------------- 1 | package lefthook 2 | 3 | import ( 4 | "path/filepath" 5 | 6 | "github.com/spf13/afero" 7 | 8 | "github.com/evilmartians/lefthook/internal/config" 9 | "github.com/evilmartians/lefthook/internal/log" 10 | ) 11 | 12 | type UninstallArgs struct { 13 | Force, RemoveConfig bool 14 | } 15 | 16 | func Uninstall(opts *Options, args *UninstallArgs) error { 17 | lefthook, err := initialize(opts) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | return lefthook.Uninstall(args) 23 | } 24 | 25 | func (l *Lefthook) Uninstall(args *UninstallArgs) error { 26 | if err := l.deleteHooks(args.Force || l.Aggressive); err != nil { 27 | return err 28 | } 29 | 30 | if err := l.Fs.Remove(l.checksumFilePath()); err == nil { 31 | log.Debugf("%s removed", l.checksumFilePath()) 32 | } else { 33 | log.Errorf("Failed removing %s: %s\n", l.checksumFilePath(), err) 34 | } 35 | 36 | if args.RemoveConfig { 37 | for _, name := range append(config.MainConfigNames, config.LocalConfigNames...) { 38 | for _, extension := range []string{ 39 | ".yml", ".yaml", ".toml", ".json", 40 | } { 41 | l.removeFile(filepath.Join(l.repo.RootPath, name+extension)) 42 | } 43 | } 44 | } 45 | 46 | return l.Fs.RemoveAll(l.repo.RemotesFolder()) 47 | } 48 | 49 | func (l *Lefthook) deleteHooks(force bool) error { 50 | hooks, err := afero.ReadDir(l.Fs, l.repo.HooksPath) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | for _, file := range hooks { 56 | hookFile := filepath.Join(l.repo.HooksPath, file.Name()) 57 | 58 | // Skip non-lefthook files if removal not forced 59 | if !l.isLefthookFile(hookFile) && !force { 60 | continue 61 | } 62 | 63 | if err := l.Fs.Remove(hookFile); err == nil { 64 | log.Debugf("%s removed", hookFile) 65 | } else { 66 | log.Errorf("Failed removing %s: %s\n", hookFile, err) 67 | } 68 | 69 | // Recover .old file if exists 70 | oldHookFile := filepath.Join(l.repo.HooksPath, file.Name()+".old") 71 | if exists, _ := afero.Exists(l.Fs, oldHookFile); !exists { 72 | continue 73 | } 74 | 75 | if err := l.Fs.Rename(oldHookFile, hookFile); err == nil { 76 | log.Debug(oldHookFile, "renamed to", file.Name()) 77 | } else { 78 | log.Errorf("Failed renaming %s: %s\n", oldHookFile, err) 79 | } 80 | } 81 | 82 | return nil 83 | } 84 | 85 | func (l *Lefthook) removeFile(glob string) { 86 | paths, err := afero.Glob(l.Fs, glob) 87 | if err != nil { 88 | log.Errorf("Failed removing configuration files: %s\n", err) 89 | return 90 | } 91 | 92 | for _, fileName := range paths { 93 | if err := l.Fs.Remove(fileName); err == nil { 94 | log.Debugf("%s removed", fileName) 95 | } else { 96 | log.Errorf("Failed removing file %s: %s\n", fileName, err) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/lefthook/validate.go: -------------------------------------------------------------------------------- 1 | package lefthook 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/kaptinlin/jsonschema" 9 | 10 | "github.com/evilmartians/lefthook/internal/config" 11 | "github.com/evilmartians/lefthook/internal/log" 12 | ) 13 | 14 | const schemaUrl = "https://raw.githubusercontent.com/evilmartians/lefthook/master/schema.json" 15 | 16 | func Validate(opts *Options) error { 17 | lefthook, err := initialize(opts) 18 | if err != nil { 19 | return fmt.Errorf("couldn't initialize lefthook: %w", err) 20 | } 21 | 22 | main, secondary, err := config.LoadKoanf(lefthook.Fs, lefthook.repo) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | compiler := jsonschema.NewCompiler() 28 | 29 | schema, err := compiler.GetSchema(schemaUrl) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | result := schema.Validate(main.Raw()) 35 | if !result.IsValid() { 36 | details := result.ToList() 37 | logValidationErrors(0, *details) 38 | 39 | return errors.New("validation failed for main config") 40 | } 41 | 42 | result = schema.Validate(secondary.Raw()) 43 | if !result.IsValid() { 44 | details := result.ToList() 45 | logValidationErrors(0, *details) 46 | 47 | return errors.New("validation failed for secondary config") 48 | } 49 | 50 | log.Info("All good") 51 | return nil 52 | } 53 | 54 | func logValidationErrors(indent int, details jsonschema.List) { 55 | if details.Valid { 56 | return 57 | } 58 | 59 | if len(details.InstanceLocation) > 0 { 60 | logDetail(indent, details) 61 | 62 | indent += 2 63 | } 64 | 65 | for _, d := range details.Details { 66 | logValidationErrors(indent, d) 67 | } 68 | } 69 | 70 | func logDetail(indent int, details jsonschema.List) { 71 | var errors []string 72 | if len(details.Errors) > 0 { 73 | for _, err := range details.Errors { 74 | errors = append(errors, err) 75 | } 76 | } 77 | 78 | option := strings.Repeat(" ", indent) + strings.TrimLeft(details.InstanceLocation, "/") + ":" 79 | 80 | if len(errors) == 0 { 81 | option = log.Gray(option) 82 | } else { 83 | option = log.Yellow(option) 84 | } 85 | 86 | if len(details.Details) > 0 { 87 | log.Info(option) 88 | } else { 89 | log.Info(option, log.Red(strings.Join(errors, ","))) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/log/builder.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type builder interface { 9 | Add(string, interface{}) builder 10 | String() string 11 | Log() 12 | } 13 | 14 | type dummyBuilder struct{} 15 | 16 | type logBuilder struct { 17 | level Level 18 | prefix string 19 | builder strings.Builder 20 | } 21 | 22 | func Builder(level Level, prefix string) builder { 23 | if !std.IsLevelEnabled(level) { 24 | return dummyBuilder{} 25 | } 26 | 27 | return &logBuilder{ 28 | prefix: prefix, 29 | level: level, 30 | builder: strings.Builder{}, 31 | } 32 | } 33 | 34 | func (b *logBuilder) Add(prefix string, data interface{}) builder { 35 | var lines []string 36 | switch v := data.(type) { 37 | case string: 38 | lines = strings.Split(strings.TrimSpace(v), "\n") 39 | case []string: 40 | lines = v 41 | default: 42 | lines = strings.Split(fmt.Sprint(data), "\n") 43 | } 44 | for i, line := range lines { 45 | line = strings.TrimSpace(line) 46 | if len(line) == 0 { 47 | continue 48 | } 49 | 50 | switch { 51 | case b.builder.Len() == 0: 52 | b.builder.WriteString(b.prefix + prefix + line + "\n") 53 | case i == 0: 54 | b.builder.WriteString(strings.Repeat(" ", len(b.prefix)) + prefix + line + "\n") 55 | default: 56 | b.builder.WriteString(strings.Repeat(" ", len(b.prefix)+len(prefix)) + line + "\n") 57 | } 58 | } 59 | 60 | return b 61 | } 62 | 63 | func (b *logBuilder) Log() { 64 | switch b.level { 65 | case DebugLevel: 66 | Debug(b.builder.String()) 67 | case InfoLevel: 68 | Info(b.builder.String()) 69 | case ErrorLevel: 70 | Error(b.builder.String()) 71 | case WarnLevel: 72 | Warn(b.builder.String()) 73 | } 74 | } 75 | 76 | func (b *logBuilder) String() string { 77 | return b.builder.String() 78 | } 79 | 80 | func (d dummyBuilder) Add(_ string, _ interface{}) builder { return d } 81 | func (dummyBuilder) Log() {} 82 | func (dummyBuilder) String() string { return "" } 83 | -------------------------------------------------------------------------------- /internal/system/limits.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import "runtime" 4 | 5 | const ( 6 | // https://serverfault.com/questions/69430/what-is-the-maximum-length-of-a-command-line-in-mac-os-x 7 | // https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation 8 | // https://unix.stackexchange.com/a/120652 9 | maxCommandLengthDarwin = 260000 // 262144 10 | maxCommandLengthWindows = 7000 // 8191, but see issues#655 11 | maxCommandLengthLinux = 130000 // 131072 12 | ) 13 | 14 | func MaxCmdLen() int { 15 | switch runtime.GOOS { 16 | case "windows": 17 | return maxCommandLengthWindows 18 | case "darwin": 19 | return maxCommandLengthDarwin 20 | default: 21 | return maxCommandLengthLinux 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/system/null_reader.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import "io" 4 | 5 | // nullReader always returns `io.EOF`. 6 | type nullReader struct{} 7 | 8 | var NullReader = nullReader{} 9 | 10 | // Implements io.Reader interface. 11 | func (nullReader) Read(b []byte) (int, error) { 12 | return 0, io.EOF 13 | } 14 | -------------------------------------------------------------------------------- /internal/system/null_reader_test.go: -------------------------------------------------------------------------------- 1 | package system 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | ) 8 | 9 | func TestNullReader(t *testing.T) { 10 | res, err := io.ReadAll(NullReader) 11 | if err != nil { 12 | t.Errorf("unexpected err: %s", err) 13 | } 14 | 15 | if !bytes.Equal(res, []byte{}) { 16 | t.Errorf("expected %v to be equal to %v", res, []byte{}) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/system/system.go: -------------------------------------------------------------------------------- 1 | // system package contains wrappers for OS interactions. 2 | package system 3 | 4 | import ( 5 | "context" 6 | "io" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | type osCmd struct { 13 | excludeEnvs []string 14 | } 15 | 16 | var Cmd = osCmd{} 17 | 18 | type Command interface { 19 | WithoutEnvs(...string) Command 20 | Run([]string, string, io.Reader, io.Writer, io.Writer) error 21 | } 22 | 23 | type CommandWithContext interface { 24 | RunWithContext(context.Context, []string, string, io.Reader, io.Writer, io.Writer) error 25 | } 26 | 27 | func (c osCmd) WithoutEnvs(envs ...string) Command { 28 | c.excludeEnvs = envs 29 | return c 30 | } 31 | 32 | func (c osCmd) Run(command []string, root string, in io.Reader, out io.Writer, errOut io.Writer) error { 33 | return c.RunWithContext(context.Background(), command, root, in, out, errOut) 34 | } 35 | 36 | // Run runs system command with LEFTHOOK=0 in order to prevent calling 37 | // subsequent lefthook hooks. 38 | func (c osCmd) RunWithContext( 39 | ctx context.Context, 40 | command []string, 41 | root string, 42 | in io.Reader, 43 | out io.Writer, 44 | errOut io.Writer, 45 | ) error { 46 | cmd := exec.CommandContext(ctx, command[0], command[1:]...) 47 | if len(c.excludeEnvs) > 0 { 48 | loop: 49 | for _, env := range os.Environ() { 50 | for _, noenv := range c.excludeEnvs { 51 | if strings.HasPrefix(env, noenv) { 52 | continue loop 53 | } 54 | } 55 | cmd.Env = append(cmd.Env, env) 56 | } 57 | cmd.Env = append(cmd.Env, "LEFTHOOK=0") 58 | } else { 59 | cmd.Env = os.Environ() 60 | cmd.Env = append(cmd.Env, "LEFTHOOK=0") 61 | } 62 | 63 | if len(root) > 0 { 64 | cmd.Dir = root 65 | } 66 | 67 | cmd.Stdin = in 68 | cmd.Stdout = out 69 | cmd.Stderr = errOut 70 | 71 | err := cmd.Run() 72 | 73 | return err 74 | } 75 | -------------------------------------------------------------------------------- /internal/templates/config.tmpl: -------------------------------------------------------------------------------- 1 | # EXAMPLE USAGE: 2 | # 3 | # Refer for explanation to following link: 4 | # https://lefthook.dev/configuration/ 5 | # 6 | # pre-push: 7 | # jobs: 8 | # - name: packages audit 9 | # tags: 10 | # - frontend 11 | # - security 12 | # run: yarn audit 13 | # 14 | # - name: gems audit 15 | # tags: 16 | # - backend 17 | # - security 18 | # run: bundle audit 19 | # 20 | # pre-commit: 21 | # parallel: true 22 | # jobs: 23 | # - run: yarn eslint {staged_files} 24 | # glob: "*.{js,ts,jsx,tsx}" 25 | # 26 | # - name: rubocop 27 | # glob: "*.rb" 28 | # exclude: 29 | # - config/application.rb 30 | # - config/routes.rb 31 | # run: bundle exec rubocop --force-exclusion {all_files} 32 | # 33 | # - name: govet 34 | # files: git ls-files -m 35 | # glob: "*.go" 36 | # run: go vet {files} 37 | # 38 | # - script: "hello.js" 39 | # runner: node 40 | # 41 | # - script: "hello.go" 42 | # runner: go run 43 | -------------------------------------------------------------------------------- /internal/templates/templates.go: -------------------------------------------------------------------------------- 1 | package templates 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "fmt" 7 | "runtime" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | const checksumFormat = "%s %d\n" 13 | 14 | //go:embed * 15 | var templatesFS embed.FS 16 | 17 | type Args struct { 18 | Rc string 19 | LefthookExe string 20 | AssertLefthookInstalled bool 21 | Roots []string 22 | } 23 | 24 | type hookTmplData struct { 25 | HookName string 26 | Extension string 27 | LefthookExe string 28 | Rc string 29 | Roots []string 30 | AssertLefthookInstalled bool 31 | } 32 | 33 | func Hook(hookName string, args Args) []byte { 34 | buf := &bytes.Buffer{} 35 | t := template.Must(template.ParseFS(templatesFS, "hook.tmpl")) 36 | err := t.Execute(buf, hookTmplData{ 37 | HookName: hookName, 38 | Extension: getExtension(), 39 | Rc: args.Rc, 40 | AssertLefthookInstalled: args.AssertLefthookInstalled, 41 | Roots: args.Roots, 42 | LefthookExe: strings.ReplaceAll(strings.TrimSpace(args.LefthookExe), "\n", ";"), 43 | }) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | return buf.Bytes() 49 | } 50 | 51 | func Config() []byte { 52 | tmpl, err := templatesFS.ReadFile("config.tmpl") 53 | if err != nil { 54 | panic(err) 55 | } 56 | 57 | return tmpl 58 | } 59 | 60 | func Checksum(checksum string, timestamp int64) []byte { 61 | return []byte(fmt.Sprintf(checksumFormat, checksum, timestamp)) 62 | } 63 | 64 | func getExtension() string { 65 | if runtime.GOOS == "windows" { 66 | return ".exe" 67 | } 68 | return "" 69 | } 70 | -------------------------------------------------------------------------------- /logo_sign.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/evilmartians/lefthook/cmd" 7 | ) 8 | 9 | func main() { 10 | os.Exit(cmd.Lefthook()) 11 | } 12 | -------------------------------------------------------------------------------- /packaging/aur/lefthook-bin/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Lefthook 2 | 3 | pkgname=lefthook-bin 4 | pkgdesc="Git hooks manager" 5 | pkgver=1.11.13 6 | pkgrel=1 7 | arch=('x86_64' 'aarch64') 8 | url="https://github.com/evilmartians/lefthook" 9 | license=('MIT') 10 | depends=() 11 | makedepends=() 12 | provides=('lefthook') 13 | conflicts=('lefthook') 14 | source_x86_64=("https://github.com/evilmartians/lefthook/releases/download/v${pkgver}/lefthook_${pkgver}_Linux_x86_64.gz") 15 | source_aarch64=("https://github.com/evilmartians/lefthook/releases/download/v${pkgver}/lefthook_${pkgver}_Linux_aarch64.gz") 16 | sha256sums_x86_64=('{{ sha256sum_linux_x86_64 }}') 17 | sha256sums_aarch64=('{{ sha256sum_linux_aarch64 }}') 18 | 19 | build() { 20 | cd "${srcdir}" 21 | 22 | mv "lefthook_${pkgver}_Linux_${CARCH}" lefthook 23 | chmod +x lefthook 24 | 25 | ./lefthook completion zsh >lefthook.zsh 26 | ./lefthook completion fish >lefthook.fish 27 | ./lefthook completion bash >lefthook.bash 28 | } 29 | 30 | package() { 31 | cd "${srcdir}" 32 | 33 | # Install lefthook 34 | install -D -m0755 lefthook \ 35 | "${pkgdir}/usr/bin/lefthook" 36 | 37 | # Install completions 38 | install -Dm644 lefthook.zsh "${pkgdir}/usr/share/zsh/site-functions/_lefthook" 39 | install -Dm644 lefthook.fish "${pkgdir}/usr/share/fish/completions/lefthook.fish" 40 | install -Dm644 lefthook.bash "${pkgdir}/usr/share/bash-completion/completions/lefthook" 41 | } 42 | -------------------------------------------------------------------------------- /packaging/aur/lefthook/PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Lefthook 2 | 3 | pkgname=lefthook 4 | pkgdesc="Git hooks manager" 5 | pkgver=1.11.13 6 | pkgrel=1 7 | arch=('x86_64' 'aarch64') 8 | url="https://github.com/evilmartians/lefthook" 9 | license=('MIT') 10 | makedepends=('go>=1.24') 11 | source=("https://github.com/evilmartians/lefthook/archive/v${pkgver}.tar.gz") 12 | sha256sums=('{{ sha256sum }}') 13 | 14 | build() { 15 | cd "$pkgname-$pkgver" 16 | go build \ 17 | -trimpath \ 18 | -buildmode=pie \ 19 | -mod=readonly \ 20 | -modcacherw \ 21 | -ldflags "-linkmode external -extldflags \"${LDFLAGS}\"" \ 22 | . 23 | } 24 | 25 | package() { 26 | cd "$pkgname-$pkgver" 27 | install -Dm755 $pkgname "$pkgdir"/usr/bin/$pkgname 28 | } 29 | -------------------------------------------------------------------------------- /packaging/npm-bundled/bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var spawn = require('child_process').spawn; 4 | const { getExePath } = require('../get-exe'); 5 | 6 | var command_args = process.argv.slice(2); 7 | 8 | var child = spawn( 9 | getExePath(), 10 | command_args, 11 | { stdio: "inherit" }); 12 | 13 | child.on('close', function (code) { 14 | if (code !== 0) { 15 | process.exit(1); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /packaging/npm-bundled/get-exe.js: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | 3 | function getExePath() { 4 | // Detect OS 5 | // https://nodejs.org/api/process.html#process_process_platform 6 | let goOS = process.platform; 7 | let extension = ''; 8 | if (['win32', 'cygwin'].includes(process.platform)) { 9 | goOS = 'windows'; 10 | extension = '.exe'; 11 | } 12 | 13 | // Detect architecture 14 | // https://nodejs.org/api/process.html#process_process_arch 15 | let goArch = process.arch; 16 | let suffix = ''; 17 | switch (process.arch) { 18 | case 'x32': 19 | case 'ia32': { 20 | goArch = '386'; 21 | break; 22 | } 23 | } 24 | 25 | const dir = path.join(__dirname, 'bin'); 26 | const executable = path.join( 27 | dir, 28 | `lefthook-${goOS}-${goArch}`, 29 | `lefthook${extension}` 30 | ); 31 | return executable; 32 | } 33 | exports.getExePath = getExePath; 34 | -------------------------------------------------------------------------------- /packaging/npm-bundled/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@evilmartians/lefthook", 3 | "version": "1.11.13", 4 | "description": "Simple git hooks manager", 5 | "main": "bin/index.js", 6 | "files": [ 7 | "postinstall.js", 8 | "get-exe.js", 9 | "schema.json", 10 | "bin/**/*" 11 | ], 12 | "bin": { 13 | "lefthook": "bin/index.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/evilmartians/lefthook.git" 18 | }, 19 | "keywords": [ 20 | "git", 21 | "hook", 22 | "manager" 23 | ], 24 | "author": "mrexox", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/evilmartians/lefthook/issues", 28 | "email": "lefthook@evilmartians.com" 29 | }, 30 | "homepage": "https://github.com/evilmartians/lefthook#readme", 31 | "os": [ 32 | "darwin", 33 | "linux", 34 | "win32" 35 | ], 36 | "cpu": [ 37 | "x64", 38 | "arm64", 39 | "ia32" 40 | ], 41 | "scripts": { 42 | "postinstall": "node postinstall.js" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packaging/npm-bundled/postinstall.js: -------------------------------------------------------------------------------- 1 | const isEnabled = (value) => value && value !== "0" && value !== "false"; 2 | if (!isEnabled(process.env.CI) || isEnabled(process.env.LEFTHOOK)) { 3 | const { spawnSync } = require('child_process'); 4 | const { getExePath } = require('./get-exe'); 5 | 6 | // run install 7 | spawnSync(getExePath(), ['install', '-f'], { 8 | cwd: process.env.INIT_CWD || process.cwd(), 9 | stdio: 'inherit', 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /packaging/npm-installer/bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var spawn = require('child_process').spawn; 4 | const path = require("path") 5 | const extension = ["win32", "cygwin"].includes(process.platform) ? ".exe" : "" 6 | const exePath = path.join(__dirname, `lefthook${extension}`) 7 | 8 | var command_args = process.argv.slice(2); 9 | var child = spawn( 10 | exePath, 11 | command_args, 12 | { stdio: "inherit" }); 13 | 14 | child.on('close', function (code) { 15 | if (code !== 0) { 16 | process.exit(1); 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /packaging/npm-installer/install.js: -------------------------------------------------------------------------------- 1 | const http = require('https') 2 | const fs = require('fs') 3 | const path = require("path") 4 | const chp = require("child_process") 5 | 6 | const iswin = ["win32", "cygwin"].includes(process.platform) 7 | 8 | async function install() { 9 | const isEnabled = (value) => value && value !== "0" && value !== "false"; 10 | if (isEnabled(process.env.CI) && !isEnabled(process.env.LEFTHOOK)) { 11 | return 12 | } 13 | const downloadURL = getDownloadURL() 14 | const extension = iswin ? ".exe" : "" 15 | const fileName = `lefthook${extension}` 16 | const exePath = path.join(__dirname, "bin", fileName) 17 | await downloadBinary(downloadURL, exePath) 18 | console.log('downloaded to', exePath) 19 | if (!iswin) { 20 | fs.chmodSync(exePath, "755") 21 | } 22 | // run install 23 | chp.spawnSync(exePath, ['install', '-f'], { 24 | cwd: process.env.INIT_CWD || process.cwd(), 25 | stdio: 'inherit', 26 | }) 27 | } 28 | 29 | function getDownloadURL() { 30 | // Detect OS 31 | // https://nodejs.org/api/process.html#process_process_platform 32 | let goOS = process.platform 33 | let extension = "" 34 | if (iswin) { 35 | goOS = "windows" 36 | extension = ".exe" 37 | } 38 | 39 | // Convert the goOS to the os name in the download URL 40 | let downloadOS = goOS === "darwin" ? "macOS" : goOS 41 | downloadOS = `${downloadOS.charAt(0).toUpperCase()}${downloadOS.slice(1)}` 42 | 43 | // Detect architecture 44 | // https://nodejs.org/api/process.html#process_process_arch 45 | let arch = process.arch 46 | switch (process.arch) { 47 | case "x64": { 48 | arch = "x86_64" 49 | break 50 | } 51 | } 52 | const version = require("./package.json").version 53 | 54 | return `https://github.com/evilmartians/lefthook/releases/download/v${version}/lefthook_${version}_${downloadOS}_${arch}${extension}` 55 | } 56 | 57 | async function downloadBinary(url, dest) { 58 | console.log('downloading', url) 59 | const file = fs.createWriteStream(dest) 60 | return new Promise((resolve, reject) => { 61 | http.get(url, function(response) { 62 | if (response.statusCode === 302 && response.headers.location) { 63 | // If the response is a 302 redirect, follow the new location 64 | downloadBinary(response.headers.location, dest) 65 | .then(resolve) 66 | .catch(reject) 67 | } else { 68 | response.pipe(file) 69 | 70 | file.on('finish', function() { 71 | file.close(() => { 72 | resolve(dest) 73 | }) 74 | }) 75 | } 76 | }).on('error', function(err) { 77 | fs.unlink(file, () => { 78 | reject(err) 79 | }) 80 | }) 81 | }) 82 | } 83 | 84 | // start: 85 | install().catch((e) => { 86 | throw e 87 | }) 88 | -------------------------------------------------------------------------------- /packaging/npm-installer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@evilmartians/lefthook-installer", 3 | "version": "1.11.13", 4 | "description": "Simple git hooks manager", 5 | "main": "bin/index.js", 6 | "files": [ 7 | "install.js", 8 | "schema.json" 9 | ], 10 | "bin": { 11 | "lefthook": "bin/index.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/evilmartians/lefthook.git" 16 | }, 17 | "keywords": [ 18 | "git", 19 | "hook", 20 | "manager" 21 | ], 22 | "author": "mrexox", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/evilmartians/lefthook/issues", 26 | "email": "lefthook@evilmartians.com" 27 | }, 28 | "homepage": "https://github.com/evilmartians/lefthook#readme", 29 | "os": [ 30 | "darwin", 31 | "linux", 32 | "win32" 33 | ], 34 | "cpu": [ 35 | "x64", 36 | "arm64" 37 | ], 38 | "scripts": { 39 | "install": "node install.js" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-darwin-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-darwin-arm64", 3 | "version": "1.11.13", 4 | "description": "The macOS ARM 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "darwin" 18 | ], 19 | "cpu": [ 20 | "arm64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-darwin-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-darwin-x64", 3 | "version": "1.11.13", 4 | "description": "The macOS 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "darwin" 18 | ], 19 | "cpu": [ 20 | "x64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-freebsd-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-freebsd-arm64", 3 | "version": "1.11.13", 4 | "description": "The FreeBSD ARM 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "freebsd" 18 | ], 19 | "cpu": [ 20 | "arm64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-freebsd-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-freebsd-x64", 3 | "version": "1.11.13", 4 | "description": "The FreeBSD 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "freebsd" 18 | ], 19 | "cpu": [ 20 | "x64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-linux-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-linux-arm64", 3 | "version": "1.11.13", 4 | "description": "The Linux ARM 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "linux" 18 | ], 19 | "cpu": [ 20 | "arm64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-linux-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-linux-x64", 3 | "version": "1.11.13", 4 | "description": "The Linux 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "linux" 18 | ], 19 | "cpu": [ 20 | "x64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-openbsd-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-openbsd-arm64", 3 | "version": "1.11.13", 4 | "description": "The OpenBSD ARM 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "openbsd" 18 | ], 19 | "cpu": [ 20 | "arm64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-openbsd-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-openbsd-x64", 3 | "version": "1.11.13", 4 | "description": "The OpenBSD 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "openbsd" 18 | ], 19 | "cpu": [ 20 | "x64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-windows-arm64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-windows-arm64", 3 | "version": "1.11.13", 4 | "description": "The Windows ARM 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "win32" 18 | ], 19 | "cpu": [ 20 | "arm64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook-windows-x64/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook-windows-x64", 3 | "version": "1.11.13", 4 | "description": "The Windows 64-bit binary for lefthook, git hooks manager.", 5 | "preferUnplugged": false, 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/evilmartians/lefthook.git" 9 | }, 10 | "license": "MIT", 11 | "bugs": { 12 | "url": "https://github.com/evilmartians/lefthook/issues", 13 | "email": "lefthook@evilmartians.com" 14 | }, 15 | "homepage": "https://github.com/evilmartians/lefthook#readme", 16 | "os": [ 17 | "win32" 18 | ], 19 | "cpu": [ 20 | "x64" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packaging/npm/lefthook/bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var spawn = require('child_process').spawn; 4 | const { getExePath } = require('../get-exe'); 5 | 6 | var command_args = process.argv.slice(2); 7 | 8 | var child = spawn( 9 | getExePath(), 10 | command_args, 11 | { stdio: "inherit" }); 12 | 13 | child.on('close', function (code) { 14 | if (code !== 0) { 15 | process.exit(1); 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /packaging/npm/lefthook/get-exe.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | function getExePath() { 4 | // Detect OS 5 | // https://nodejs.org/api/process.html#process_process_platform 6 | let os = process.platform; 7 | let extension = ""; 8 | if (["win32", "cygwin"].includes(process.platform)) { 9 | os = "windows"; 10 | extension = ".exe"; 11 | } 12 | 13 | // Detect architecture 14 | // https://nodejs.org/api/process.html#process_process_arch 15 | let arch = process.arch; 16 | 17 | return require.resolve(`lefthook-${os}-${arch}/bin/lefthook${extension}`); 18 | } 19 | 20 | exports.getExePath = getExePath; 21 | -------------------------------------------------------------------------------- /packaging/npm/lefthook/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lefthook", 3 | "version": "1.11.13", 4 | "description": "Simple git hooks manager", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/evilmartians/lefthook.git" 8 | }, 9 | "main": "bin/index.js", 10 | "files": [ 11 | "postinstall.js", 12 | "get-exe.js", 13 | "schema.json" 14 | ], 15 | "bin": { 16 | "lefthook": "bin/index.js" 17 | }, 18 | "keywords": [ 19 | "git", 20 | "hook", 21 | "manager" 22 | ], 23 | "author": "mrexox", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/evilmartians/lefthook/issues", 27 | "email": "lefthook@evilmartians.com" 28 | }, 29 | "homepage": "https://github.com/evilmartians/lefthook#readme", 30 | "optionalDependencies": { 31 | "lefthook-darwin-arm64": "1.11.13", 32 | "lefthook-darwin-x64": "1.11.13", 33 | "lefthook-linux-arm64": "1.11.13", 34 | "lefthook-linux-x64": "1.11.13", 35 | "lefthook-freebsd-arm64": "1.11.13", 36 | "lefthook-freebsd-x64": "1.11.13", 37 | "lefthook-openbsd-arm64": "1.11.13", 38 | "lefthook-openbsd-x64": "1.11.13", 39 | "lefthook-windows-arm64": "1.11.13", 40 | "lefthook-windows-x64": "1.11.13" 41 | }, 42 | "scripts": { 43 | "postinstall": "node postinstall.js" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packaging/npm/lefthook/postinstall.js: -------------------------------------------------------------------------------- 1 | const { spawnSync } = require("child_process"); 2 | const { getExePath } = require("./get-exe"); 3 | 4 | function install() { 5 | const isEnabled = (value) => value && value !== "0" && value !== "false"; 6 | if (isEnabled(process.env.CI) && !isEnabled(process.env.LEFTHOOK)) { 7 | return 8 | } 9 | 10 | spawnSync(getExePath(), ["install", "-f"], { 11 | cwd: process.env.INIT_CWD || process.cwd(), 12 | stdio: "inherit", 13 | }); 14 | } 15 | 16 | try { 17 | install(); 18 | } catch (e) { 19 | console.warn( 20 | "'lefthook install' command failed. Try running it manually.\n" + e, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packaging/pypi/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2019 Arkweid 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /packaging/pypi/README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://github.com/evilmartians/lefthook/actions/workflows/test.yml/badge.svg?branch=master) 2 | [![Coverage Status](https://coveralls.io/repos/github/evilmartians/lefthook/badge.svg?branch=master)](https://coveralls.io/github/evilmartians/lefthook?branch=master) 3 | 4 | # Lefthook 5 | 6 | > The fastest polyglot Git hooks manager out there 7 | 8 | 10 | 11 | A Git hooks manager for Node.js, Ruby and many other types of projects. 12 | 13 | * **Fast.** It is written in Go. Can run commands in parallel. 14 | * **Powerful.** It allows to control execution and files you pass to your commands. 15 | * **Simple.** It is single dependency-free binary which can work in any environment. 16 | 17 | 📖 [Read the introduction post](https://evilmartians.com/chronicles/lefthook-knock-your-teams-code-back-into-shape?utm_source=lefthook) 18 | 19 | 20 | Sponsored by Evil Martians 21 | 22 | ## Install 23 | 24 | ```bash 25 | pip install lefthook 26 | ``` 27 | 28 | ## Usage 29 | 30 | Configure your hooks, install them once and forget about it: rely on the magic underneath. 31 | 32 | #### TL;DR 33 | 34 | ```bash 35 | # Configure your hooks 36 | vim lefthook.yml 37 | 38 | # Install them to the git project 39 | lefthook install 40 | 41 | # Enjoy your work with git 42 | git add -A && git commit -m '...' 43 | ``` 44 | 45 | #### More details 46 | 47 | - [**Configuration**](https://github.com/evilmartians/lefthook/blob/master/docs/configuration.md) for `lefthook.yml` config options. 48 | - [**Usage**](https://github.com/evilmartians/lefthook/blob/master/docs/usage.md) for **lefthook** CLI options, supported ENVs, and usage tips. 49 | - [**Discussions**](https://github.com/evilmartians/lefthook/discussions) for questions, ideas, suggestions. 50 | 51 | -------------------------------------------------------------------------------- /packaging/pypi/lefthook/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evilmartians/lefthook/d1eb3be2522a2afa0da6cfa255d7843935af6812/packaging/pypi/lefthook/__init__.py -------------------------------------------------------------------------------- /packaging/pypi/lefthook/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from .main import main 4 | 5 | if __name__ == "__main__": 6 | sys.exit(main()) 7 | -------------------------------------------------------------------------------- /packaging/pypi/lefthook/bin/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evilmartians/lefthook/d1eb3be2522a2afa0da6cfa255d7843935af6812/packaging/pypi/lefthook/bin/.keep -------------------------------------------------------------------------------- /packaging/pypi/lefthook/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import platform 4 | import subprocess 5 | 6 | ISSUE_URL = "https://github.com/evilmartians/lefthook/issues/new/choose" 7 | ARCH_MAPPING = { 8 | 'amd64': 'x86_64', 9 | 'aarch64': 'arm64', 10 | } 11 | 12 | def main(): 13 | os_name = platform.system().lower() 14 | arch = platform.machine().lower() 15 | arch = ARCH_MAPPING.get(arch, arch) 16 | ext = os_name == "windows" and ".exe" or "" 17 | subfolder = f"lefthook-{os_name}-{arch}" 18 | executable = os.path.join(os.path.dirname(__file__), "bin", subfolder, "lefthook"+ext) 19 | if not os.path.isfile(executable): 20 | print(f"Couldn't find binary {executable}. Please create an issue: {ISSUE_URL}") 21 | return 1 22 | 23 | result = subprocess.run([executable] + sys.argv[1:]) 24 | return result.returncode 25 | -------------------------------------------------------------------------------- /packaging/pypi/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setup( 7 | name='lefthook', 8 | version='1.11.13', 9 | author='Evil Martians', 10 | author_email='lefthook@evilmartians.com', 11 | url='https://github.com/evilmartians/lefthook', 12 | description='Git hooks manager. Fast, powerful, simple.', 13 | long_description=long_description, 14 | long_description_content_type="text/markdown", 15 | packages=find_packages(), 16 | entry_points={ 17 | 'console_scripts': [ 18 | 'lefthook=lefthook.main:main' 19 | ], 20 | }, 21 | package_data={ 22 | 'lefthook':[ 23 | 'bin/lefthook-linux-x86_64/lefthook', 24 | 'bin/lefthook-linux-arm64/lefthook', 25 | 'bin/lefthook-freebsd-x86_64/lefthook', 26 | 'bin/lefthook-freebsd-arm64/lefthook', 27 | 'bin/lefthook-openbsd-x86_64/lefthook', 28 | 'bin/lefthook-openbsd-arm64/lefthook', 29 | 'bin/lefthook-windows-x86_64/lefthook.exe', 30 | 'bin/lefthook-windows-arm64/lefthook.exe', 31 | 'bin/lefthook-darwin-x86_64/lefthook', 32 | 'bin/lefthook-darwin-arm64/lefthook', 33 | ] 34 | }, 35 | classifiers=[ 36 | 'License :: OSI Approved :: MIT License', 37 | 'Operating System :: OS Independent', 38 | 'Topic :: Software Development :: Version Control :: Git' 39 | ], 40 | python_requires='>=3.6', 41 | ) 42 | -------------------------------------------------------------------------------- /packaging/rubygems/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in lefthook.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /packaging/rubygems/README.md: -------------------------------------------------------------------------------- 1 | # Lefthook 2 | 3 | Ruby wrapper around [lefthook](https://github.com/evilmartians/lefthook) 4 | -------------------------------------------------------------------------------- /packaging/rubygems/Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | task :default => :spec 3 | -------------------------------------------------------------------------------- /packaging/rubygems/bin/lefthook: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "rubygems" 4 | 5 | platform = Gem::Platform.new(RUBY_PLATFORM) 6 | arch = 7 | case platform.cpu.sub(/\Auniversal\./, '') 8 | when /\Aarm64/ then "arm64" # Apple reports arm64e on M1 macs 9 | when /aarch64/ then "arm64" 10 | when "x86_64" then "x64" 11 | when "x64" then "x64" # Windows with MINGW64 reports RUBY_PLATFORM as "x64-mingw32" 12 | else raise "Unknown architecture: #{platform.cpu}" 13 | end 14 | 15 | os = 16 | case platform.os 17 | when "linux" then "linux" 18 | when "darwin" then "darwin" # MacOS 19 | when "windows" then "windows" 20 | when "mingw32" then "windows" # Windows with MINGW64 reports RUBY_PLATFORM as "x64-mingw32" 21 | when "mingw" then "windows" 22 | when "freebsd" then "freebsd" 23 | when "openbsd" then "openbsd" 24 | else raise "Unknown OS: #{platform.os}" 25 | end 26 | 27 | binary = "lefthook-#{os}-#{arch}/lefthook" 28 | binary = "#{binary}.exe" if os == "windows" 29 | 30 | args = $*.map { |x| x.include?(' ') ? "'" + x + "'" : x } 31 | cmd = File.expand_path "#{File.dirname(__FILE__)}/../libexec/#{binary}" 32 | 33 | unless File.exist?(cmd) 34 | raise "Invalid platform. Lefthook wasn't build for #{RUBY_PLATFORM}" 35 | end 36 | 37 | pid = spawn("#{cmd} #{args.join(' ')}") 38 | Process.wait(pid) 39 | exit($?.exitstatus) 40 | -------------------------------------------------------------------------------- /packaging/rubygems/lefthook.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "lefthook" 3 | spec.version = "1.11.13" 4 | spec.authors = ["A.A.Abroskin", "Evil Martians"] 5 | spec.email = ["lefthook@evilmartians.com"] 6 | 7 | spec.summary = "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." 8 | spec.homepage = "https://github.com/evilmartians/lefthook" 9 | spec.post_install_message = "Lefthook installed! Run command in your project root directory 'lefthook install -f' to complete installation." 10 | 11 | spec.bindir = "bin" 12 | spec.executables << "lefthook" 13 | spec.require_paths = ["lib"] 14 | 15 | spec.files = %w( 16 | lib/lefthook.rb 17 | bin/lefthook 18 | ) + `find libexec/ -executable -type f -print0`.split("\x0") 19 | 20 | spec.licenses = ['MIT'] 21 | end 22 | -------------------------------------------------------------------------------- /packaging/rubygems/lib/lefthook.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evilmartians/lefthook/d1eb3be2522a2afa0da6cfa255d7843935af6812/packaging/rubygems/lib/lefthook.rb -------------------------------------------------------------------------------- /packaging/rubygems/libexec/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evilmartians/lefthook/d1eb3be2522a2afa0da6cfa255d7843935af6812/packaging/rubygems/libexec/.keep -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0xb1a25Fe215747E1093901282dc2Ea68cE8c290D8' 6 | - '0x4d9B3A6207B48E31147327f8efaF31D5EFC3784e' 7 | quorum: 1 8 | -------------------------------------------------------------------------------- /testdata/add.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec lefthook add pre-commit 5 | ! stderr . 6 | exists .git/hooks/pre-commit 7 | ! exists .lefthook/pre-commit 8 | 9 | exec lefthook add pre-push --dirs 10 | ! stderr . 11 | exists .git/hooks/pre-push 12 | exists .lefthook/pre-push 13 | exists .lefthook-local/pre-push 14 | -------------------------------------------------------------------------------- /testdata/cli_run_only.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec git config user.email "you@example.com" 3 | exec git config user.name "Your Name" 4 | exec git add -A 5 | exec lefthook install 6 | exec lefthook run hook --jobs a --jobs c --jobs db --commands lint 7 | stdout '\s*a\s*ca\s*cb\s*db\s*lint\s*' 8 | 9 | -- lefthook.yml -- 10 | output: 11 | - execution_out 12 | hook: 13 | jobs: 14 | - name: a 15 | run: echo a 16 | - name: b 17 | run: echo b 18 | - name: c 19 | group: 20 | jobs: 21 | - run: echo ca 22 | - run: echo cb 23 | - name: d 24 | group: 25 | jobs: 26 | - name: da 27 | run: echo da 28 | - name: db 29 | run: echo db 30 | commands: 31 | lint: 32 | run: echo lint 33 | test: 34 | run: echo test 35 | -------------------------------------------------------------------------------- /testdata/exclude.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook install 3 | exec git config user.email "you@example.com" 4 | exec git config user.name "Your Name" 5 | exec git add -A 6 | exec lefthook run -f all 7 | stdout '^a.txt b.txt dir/a.txt dir/b.txt lefthook.yml\s*$' 8 | exec lefthook run -f regexp 9 | stdout '^dir/a.txt dir/b.txt lefthook.yml\s*$' 10 | exec lefthook run -f array 11 | stdout '^dir/a.txt dir/b.txt\s*$' 12 | exec lefthook run -f nested 13 | stdout '^lefthook.yml\s+dir/b.txt lefthook.yml\s+b.txt dir/b.txt lefthook.yml\s+b.txt dir/b.txt\s*$' 14 | 15 | -- lefthook.yml -- 16 | skip_output: 17 | - skips 18 | - meta 19 | - summary 20 | - execution_info 21 | all: 22 | commands: 23 | echo: 24 | run: echo {staged_files} 25 | 26 | regexp: 27 | commands: 28 | echo: 29 | run: echo {staged_files} 30 | exclude: '^(a.txt|b.txt)' 31 | 32 | array: 33 | jobs: 34 | - run: echo {staged_files} 35 | exclude: 36 | - a.txt 37 | - b.txt 38 | - '*.yml' 39 | 40 | nested: 41 | jobs: 42 | - exclude: 43 | - '*.txt' 44 | run: echo {staged_files} 45 | - exclude: 46 | - a.txt 47 | - dir/a.txt 48 | group: 49 | jobs: 50 | - exclude: 51 | - b.txt 52 | run: echo {staged_files} 53 | - group: 54 | jobs: 55 | - run: echo {staged_files} 56 | - group: 57 | jobs: 58 | - exclude: 59 | - '*.yml' 60 | run: echo {staged_files} 61 | -- a.txt -- 62 | a 63 | 64 | -- b.txt -- 65 | b 66 | 67 | -- dir/a.txt -- 68 | dir-a 69 | 70 | -- dir/b.txt -- 71 | dir-b 72 | 73 | 74 | -------------------------------------------------------------------------------- /testdata/fail_text.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec git config user.email "you@example.com" 3 | exec git config user.name "Your Name" 4 | exec git add -A 5 | exec lefthook install 6 | ! exec git commit -m 'test' 7 | stderr '\s*fails: no such command\s*' 8 | 9 | -- lefthook.yml -- 10 | output: 11 | - failure 12 | pre-commit: 13 | commands: 14 | fails: 15 | run: oops-no-such-command 16 | fail_text: no such command 17 | -------------------------------------------------------------------------------- /testdata/files_override.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook install 3 | exec git add -A 4 | 5 | exec lefthook run echo 6 | stdout 'a-file\.js' 7 | 8 | exec lefthook run echo --all-files 9 | stdout 'a-file\.js b_file\.go c,file\.rb' 10 | 11 | exec lefthook run echo --file a-file.js --file ghost.file 12 | stdout 'a-file\.js ghost\.file' 13 | 14 | -- lefthook.yml -- 15 | skip_output: 16 | - meta 17 | - execution_info 18 | - summary 19 | 20 | echo: 21 | commands: 22 | echo: 23 | files: echo a-file.js 24 | run: echo "{files}" 25 | 26 | -- a-file.js -- 27 | a-file.js 28 | 29 | -- b_file.go -- 30 | b_file.go 31 | 32 | -- c,file.rb -- 33 | c,file.rb 34 | -------------------------------------------------------------------------------- /testdata/filter_by_file_type.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec git config user.email "you@example.com" 5 | exec git config user.name "Your Name" 6 | exec lefthook install 7 | chmod 777 executable 8 | symlink symlink -> results 9 | exec git add -A 10 | exec git commit -m 'test' 11 | exec lefthook run filters 12 | stdout '.*all ❯\s+executable lefthook.yml results symlink\s+┃.*' 13 | stdout '.*filter_text ❯\s+executable lefthook.yml results\s+┃.*' 14 | stdout '.*filter_executable ❯\s+executable\s+┃.*' 15 | stdout '.*filter_symlink ❯\s+symlink\s+┃.*' 16 | stdout '.*filter_not_symlink ❯\s+executable lefthook.yml results\s+┃.*' 17 | stdout '.*filter_not_executable ❯\s+lefthook.yml results symlink\s*' 18 | 19 | -- lefthook.yml -- 20 | output: 21 | - execution 22 | - skips 23 | filters: 24 | piped: true 25 | commands: 26 | all: 27 | run: echo {all_files} 28 | priority: 1 29 | filter_text: 30 | run: echo {all_files} 31 | file_types: text 32 | priority: 2 33 | filter_executable: 34 | run: echo {all_files} 35 | file_types: executable 36 | priority: 3 37 | filter_symlink: 38 | run: echo {all_files} 39 | file_types: symlink 40 | priority: 4 41 | filter_not_symlink: 42 | run: echo {all_files} 43 | file_types: not symlink 44 | priority: 5 45 | filter_not_executable: 46 | run: echo {all_files} 47 | priority: 6 48 | file_types: 49 | - not executable 50 | 51 | -- results -- 52 | some text 53 | 54 | -- executable -- 55 | #!/bin/sh 56 | 57 | echo 'Executable' 58 | -------------------------------------------------------------------------------- /testdata/hide_unstaged.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec lefthook install 5 | exec git config user.email "you@example.com" 6 | exec git config user.name "Your Name" 7 | 8 | exec lefthook run edit_file 9 | exec git add -A 10 | exec lefthook run edit_file 11 | exec git status --short 12 | stdout 'AM newfile.txt' 13 | 14 | exec git commit -m 'test hide unstaged changes' 15 | exec git status --short 16 | stdout 'M newfile.txt' 17 | 18 | -- lefthook.yml -- 19 | min_version: 1.1.1 20 | pre-commit: 21 | commands: 22 | edit_file: 23 | run: echo newline >> file.txt 24 | stage_fixed: true 25 | 26 | edit_file: 27 | commands: 28 | echo: 29 | run: echo newline >> newfile.txt 30 | 31 | -- file.txt -- 32 | firstline 33 | -------------------------------------------------------------------------------- /testdata/install.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook install 3 | exists lefthook.yml 4 | ! stderr . 5 | -------------------------------------------------------------------------------- /testdata/job_fail_text.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec git config user.email "you@example.com" 3 | exec git config user.name "Your Name" 4 | exec git add -A 5 | exec lefthook install 6 | ! exec git commit -m 'test' 7 | stderr '\s*fails: no such command\s*' 8 | 9 | -- lefthook.yml -- 10 | output: 11 | - failure 12 | pre-commit: 13 | jobs: 14 | - name: fails 15 | run: oops-no-such-command 16 | fail_text: no such command 17 | -------------------------------------------------------------------------------- /testdata/job_filter_by_file_type.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec git config user.email "you@example.com" 5 | exec git config user.name "Your Name" 6 | exec lefthook install 7 | chmod 777 executable 8 | symlink symlink -> results 9 | exec git add -A 10 | exec git commit -m 'test' 11 | exec lefthook run filters 12 | stdout '.*all ❯\s+executable lefthook.yml results symlink\s+┃.*' 13 | stdout '.*filter_text ❯\s+executable lefthook.yml results\s+┃.*' 14 | stdout '.*filter_executable ❯\s+executable\s+┃.*' 15 | stdout '.*filter_symlink ❯\s+symlink\s+┃.*' 16 | stdout '.*filter_not_symlink ❯\s+executable lefthook.yml results\s+┃.*' 17 | stdout '.*filter_not_executable ❯\s+lefthook.yml results symlink\s*' 18 | 19 | -- lefthook.yml -- 20 | output: 21 | - execution 22 | - skips 23 | filters: 24 | piped: true 25 | jobs: 26 | - name: all 27 | run: echo {all_files} 28 | 29 | - name: filter_text 30 | run: echo {all_files} 31 | file_types: text 32 | 33 | - name: filter_executable 34 | run: echo {all_files} 35 | file_types: executable 36 | 37 | - name: filter_symlink 38 | run: echo {all_files} 39 | file_types: symlink 40 | 41 | - name: filter_not_symlink 42 | run: echo {all_files} 43 | file_types: not symlink 44 | 45 | - name: filter_not_executable 46 | run: echo {all_files} 47 | file_types: 48 | - not executable 49 | 50 | -- results -- 51 | some text 52 | 53 | -- executable -- 54 | #!/bin/sh 55 | 56 | echo 'Executable' 57 | 58 | -------------------------------------------------------------------------------- /testdata/job_merging.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec lefthook dump 5 | cmp stdout dump.yml 6 | ! stderr . 7 | 8 | -- lefthook.yml -- 9 | extends: 10 | - extends/e1.yml 11 | 12 | pre-commit: 13 | jobs: 14 | - name: group 15 | group: 16 | jobs: 17 | - name: child 18 | run: named 19 | - run: 0 no-name 20 | - name: echo 21 | run: echo 0 22 | - run: lefthook.yml 23 | 24 | -- extends/e1.yml -- 25 | extends: 26 | - extends/e2.yml 27 | 28 | pre-commit: 29 | jobs: 30 | - name: group 31 | group: 32 | jobs: 33 | - name: child 34 | run: child named 35 | - run: 1 no-name 36 | - name: echo 37 | run: echo 1 38 | skip: true 39 | - run: e1 40 | 41 | e1: 42 | jobs: 43 | - name: echo 44 | run: e1 45 | 46 | -- extends/e2.yml -- 47 | extends: 48 | - extends/e3.yml 49 | 50 | pre-commit: 51 | jobs: 52 | - name: group 53 | glob: "*.rb" 54 | group: 55 | jobs: 56 | - name: child 57 | run: child named with glob 58 | - run: 2 no-name 59 | - name: echo 60 | run: echo 2 61 | tags: ["backend"] 62 | - run: e2 63 | 64 | e2: 65 | jobs: 66 | - name: echo 67 | run: e2 68 | 69 | -- extends/e3.yml -- 70 | pre-commit: 71 | jobs: 72 | - name: group 73 | glob: "*.rb" 74 | group: 75 | jobs: 76 | - name: child 77 | stage_fixed: true 78 | - run: 3 no-name 79 | - name: echo 80 | glob: 3 81 | - run: e3 82 | 83 | e3: 84 | jobs: 85 | - name: echo 86 | run: e3 87 | 88 | -- dump.yml -- 89 | e1: 90 | jobs: 91 | - name: echo 92 | run: e1 93 | e2: 94 | jobs: 95 | - name: echo 96 | run: e2 97 | e3: 98 | jobs: 99 | - name: echo 100 | run: e3 101 | extends: 102 | - extends/e3.yml 103 | pre-commit: 104 | jobs: 105 | - name: group 106 | glob: 107 | - '*.rb' 108 | group: 109 | jobs: 110 | - name: child 111 | run: child named with glob 112 | stage_fixed: true 113 | - run: 0 no-name 114 | - run: 1 no-name 115 | - run: 2 no-name 116 | - run: 3 no-name 117 | - name: echo 118 | run: echo 2 119 | glob: 120 | - "3" 121 | tags: 122 | - backend 123 | skip: true 124 | - run: lefthook.yml 125 | - run: e1 126 | - run: e2 127 | - run: e3 128 | -------------------------------------------------------------------------------- /testdata/job_stage_fixed.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook install 3 | exec git config user.email "you@example.com" 4 | exec git config user.name "Your Name" 5 | exec git add -A 6 | exec git status --short 7 | exec git commit -m 'test stage_fixed' 8 | exec git status --short 9 | ! stdout . 10 | 11 | -- lefthook.yml -- 12 | min_version: 1.1.1 13 | pre-commit: 14 | jobs: 15 | - stage_fixed: true 16 | run: | 17 | echo newline >> "[file].js" 18 | echo newline >> file.txt 19 | 20 | -- file.txt -- 21 | sometext 22 | 23 | -- [file].js -- 24 | somecode 25 | -------------------------------------------------------------------------------- /testdata/lefthook_option.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook install 3 | exec git config user.email "you@example.com" 4 | exec git config user.name "Your Name" 5 | exec git add -A 6 | exec git commit -m 'must show debug logs' 7 | stderr 'injected' 8 | stdout '[lefthook]' 9 | 10 | -- lefthook.yml -- 11 | lefthook: | 12 | echo 'injected' 13 | lefthook -v 14 | 15 | output: 16 | - execution 17 | pre-commit: 18 | jobs: 19 | - run: echo {all_files} 20 | glob: "*.txt" 21 | 22 | -- file.txt -- 23 | sometext 24 | 25 | -- file.js -- 26 | somecode 27 | -------------------------------------------------------------------------------- /testdata/many_extends_levels.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec lefthook dump 5 | cmp stdout dump.yml 6 | ! stderr . 7 | 8 | -- lefthook.yml -- 9 | extends: 10 | - extends/e1.yml 11 | 12 | pre-commit: 13 | commands: 14 | echo: 15 | run: echo 0 16 | 17 | -- extends/e1.yml -- 18 | extends: 19 | - extends/e2.yml 20 | 21 | pre-commit: 22 | commands: 23 | echo: 24 | run: echo 1 25 | skip: true 26 | 27 | e1: 28 | commands: 29 | echo: 30 | run: e1 31 | 32 | -- extends/e2.yml -- 33 | extends: 34 | - extends/e3.yml 35 | 36 | pre-commit: 37 | commands: 38 | echo: 39 | run: echo 2 40 | tags: ["backend"] 41 | 42 | e2: 43 | commands: 44 | echo: 45 | run: e2 46 | 47 | -- extends/e3.yml -- 48 | pre-commit: 49 | commands: 50 | echo: 51 | glob: 3 52 | 53 | e3: 54 | commands: 55 | echo: 56 | run: e3 57 | 58 | -- dump.yml -- 59 | e1: 60 | commands: 61 | echo: 62 | run: e1 63 | e2: 64 | commands: 65 | echo: 66 | run: e2 67 | e3: 68 | commands: 69 | echo: 70 | run: e3 71 | extends: 72 | - extends/e3.yml 73 | pre-commit: 74 | commands: 75 | echo: 76 | run: echo 2 77 | skip: true 78 | tags: 79 | - backend 80 | glob: 81 | - "3" 82 | -------------------------------------------------------------------------------- /testdata/min_version.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | ! exec lefthook run pre-commit 3 | stdout 'required lefthook version \(13.1.1\) is higher than current' 4 | 5 | -- lefthook.yml -- 6 | min_version: 13.1.1 7 | pre-commit: 8 | commands: 9 | echo: 10 | run: echo 11 | -------------------------------------------------------------------------------- /testdata/pre-commit_issue_919.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec git add -A 3 | exec git config user.email "you@example.com" 4 | exec git config user.name "Your Name" 5 | exec git add -A 6 | exec git commit -m 'first commit' 7 | rm file.txt 8 | exec git add -A 9 | exec lefthook run pre-commit 10 | stdout '^\s*must be printed\s*$' 11 | 12 | -- lefthook.yml -- 13 | output: 14 | - execution_out 15 | pre-commit: 16 | jobs: 17 | - run: echo 'must be printed' 18 | - run: echo 'excluded txt' 19 | exclude: 20 | - '*.txt' 21 | - run: echo 'excluded by' {staged_files} 22 | 23 | -- file.txt -- 24 | will be deleted 25 | -------------------------------------------------------------------------------- /testdata/remote.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec lefthook install 5 | 6 | exec lefthook dump 7 | cmp stdout lefthook-dump.yml 8 | 9 | -- lefthook.yml -- 10 | remote: 11 | git_url: https://github.com/evilmartians/lefthook 12 | config: examples/with_scripts/lefthook.yml 13 | ref: v1.4.0 14 | 15 | -- lefthook-dump.yml -- 16 | │ DEPRECATED: "remote" option is deprecated and will be omitted in the next major release, use "remotes" option instead 17 | │ DEPRECATED: "remotes"."config" option is deprecated and will be omitted in the next major release, use "configs" option instead 18 | pre-commit: 19 | scripts: 20 | good_job.js: 21 | runner: node 22 | remotes: 23 | - git_url: https://github.com/evilmartians/lefthook 24 | ref: v1.4.0 25 | configs: 26 | - examples/with_scripts/lefthook.yml 27 | -------------------------------------------------------------------------------- /testdata/remotes.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec lefthook install 5 | 6 | exec lefthook dump 7 | cmp stdout lefthook-dump.yml 8 | 9 | -- lefthook.yml -- 10 | remotes: 11 | - git_url: https://github.com/evilmartians/lefthook 12 | config: examples/with_scripts/lefthook.yml 13 | ref: v1.4.0 14 | - git_url: https://github.com/evilmartians/lefthook 15 | configs: 16 | - examples/verbose/lefthook.yml 17 | - examples/remote/ping.yml 18 | 19 | -- lefthook-dump.yml -- 20 | │ DEPRECATED: "remotes"."config" option is deprecated and will be omitted in the next major release, use "configs" option instead 21 | pre-commit: 22 | parallel: true 23 | commands: 24 | js-lint: 25 | run: npx eslint --fix {staged_files} && git add {staged_files} 26 | glob: 27 | - '*.{js,ts}' 28 | ping: 29 | run: echo pong 30 | ruby-lint: 31 | run: bundle exec rubocop --force-exclusion --parallel '{files}' 32 | files: git diff-tree -r --name-only --diff-filter=CDMR HEAD origin/master 33 | glob: 34 | - '*.rb' 35 | ruby-test: 36 | run: bundle exec rspec 37 | skip: 38 | - merge 39 | - rebase 40 | fail_text: Run bundle install 41 | scripts: 42 | good_job.js: 43 | runner: node 44 | pre-push: 45 | commands: 46 | spelling: 47 | run: npx yaspeller {files} 48 | files: git diff --name-only HEAD @{push} 49 | glob: 50 | - '*.md' 51 | remotes: 52 | - git_url: https://github.com/evilmartians/lefthook 53 | ref: v1.4.0 54 | configs: 55 | - examples/with_scripts/lefthook.yml 56 | - git_url: https://github.com/evilmartians/lefthook 57 | configs: 58 | - examples/verbose/lefthook.yml 59 | - examples/remote/ping.yml 60 | -------------------------------------------------------------------------------- /testdata/run_interrupt.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | chmod 0700 hook.sh 4 | chmod 0700 commit-with-interrupt.sh 5 | exec git init 6 | exec git config user.email "you@example.com" 7 | exec git config user.name "Your Name" 8 | exec lefthook install 9 | exec git add -A 10 | 11 | exec git commit -m 'init' 12 | stderr 'hook-done' 13 | 14 | exec ./commit-with-interrupt.sh 15 | stderr 'script-done' 16 | ! stderr 'hook-done' 17 | stderr 'signal: killed' 18 | stderr 'Error: Interrupted' 19 | grep unstaged newfile.txt 20 | exec git stash list 21 | ! stdout 'lefthook auto backup' 22 | 23 | -- lefthook.yml -- 24 | pre-commit: 25 | commands: 26 | slow_job: 27 | run: ./hook.sh 28 | 29 | -- hook.sh -- 30 | #!/usr/bin/env bash 31 | 32 | sleep 2 33 | >&2 echo hook-done 34 | 35 | -- newfile.txt -- 36 | staged 37 | 38 | -- commit-with-interrupt.sh -- 39 | #!/usr/bin/env bash 40 | 41 | echo staged >> newfile.txt 42 | git add newfile.txt 43 | echo unstaged >> newfile.txt 44 | 45 | # ctrl-c is emulated by sending SIGINT to a process group 46 | # so we first need to emulate being a terminal and enable 47 | # job monitoring so that new PGIDs are assigned. 48 | set -m 49 | nohup git commit -m test & 50 | pgid=$! 51 | sleep 1 52 | kill -SIGINT -$pgid 53 | wait 54 | >&2 echo 'script-done' 55 | -------------------------------------------------------------------------------- /testdata/run_json.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec git config user.email "you@example.com" 3 | exec git config user.name "Your Name" 4 | exec git add -A 5 | exec lefthook install 6 | exec git commit -m 'test' 7 | stderr '\s*Hi there from Lefthook\s*' 8 | 9 | -- lefthook.json -- 10 | { 11 | "skip_output": [ 12 | "meta", 13 | "summary", 14 | "execution_info" 15 | ], 16 | "pre-commit": { 17 | "commands": { 18 | "echo": { 19 | "run": "echo Hi there from Lefthook" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /testdata/run_non_existing.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook run pre-commit 3 | ! stdout 'Error.*' 4 | ! exec lefthook run no-a-hook 5 | stdout 'Error.*' 6 | 7 | -- lefthook.yml -- 8 | # empty 9 | -------------------------------------------------------------------------------- /testdata/run_script.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec git config user.email "you@example.com" 3 | exec git config user.name "Your Name" 4 | exec git add -A 5 | exec lefthook install 6 | exec git commit -m 'test' 7 | stderr '\s*Hi there from script\s*' 8 | 9 | -- lefthook.yml -- 10 | skip_output: 11 | - meta 12 | - summary 13 | - execution_info 14 | pre-commit: 15 | scripts: 16 | "file.sh": 17 | runner: sh 18 | 19 | -- .lefthook/pre-commit/file.sh -- 20 | #!/usr/bin/env sh 21 | 22 | echo Hi there from scripts 23 | -------------------------------------------------------------------------------- /testdata/run_toml.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec git config user.email "you@example.com" 3 | exec git config user.name "Your Name" 4 | exec git add -A 5 | exec lefthook install 6 | exec git commit -m 'test' 7 | stderr '\s*Hi there from Lefthook\s*' 8 | 9 | -- lefthook.toml -- 10 | skip_output = [ 11 | 'meta', 12 | 'summary', 13 | 'execution_info', 14 | ] 15 | 16 | [pre-commit.commands.echo] 17 | run = "echo Hi there from Lefthook" 18 | -------------------------------------------------------------------------------- /testdata/run_yml.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec git config user.email "you@example.com" 3 | exec git config user.name "Your Name" 4 | exec git add -A 5 | exec lefthook install 6 | exec git commit -m 'test' 7 | stderr '\s*Hi there from Lefthook\s*' 8 | 9 | -- lefthook.yml -- 10 | skip_output: 11 | - meta 12 | - summary 13 | - execution_info 14 | pre-commit: 15 | commands: 16 | echo: 17 | run: echo Hi there from Lefthook 18 | -------------------------------------------------------------------------------- /testdata/sh_syntax_in_files.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec lefthook install 5 | exec git config user.email "you@example.com" 6 | exec git config user.name "Your Name" 7 | 8 | exec lefthook run echo_files 9 | 10 | stdout '1.txt 10.txt' 11 | 12 | -- lefthook.yml -- 13 | skip_output: 14 | - meta # Skips lefthook version printing 15 | - summary # Skips summary block (successful and failed steps) printing 16 | - empty_summary # Skips summary heading when there are no steps to run 17 | - success # Skips successful steps printing 18 | - failure # Skips failed steps printing 19 | - execution_info # Skips printing `EXECUTE > ...` logging 20 | - skips 21 | 22 | echo_files: 23 | commands: 24 | echo: 25 | files: ls | grep 1 26 | run: echo {files} 27 | 28 | -- 1.txt -- 29 | 1.txt 30 | 31 | -- 10.txt -- 32 | 10.txt 33 | 34 | -- 20.txt -- 35 | 20.txt 36 | -------------------------------------------------------------------------------- /testdata/skip_merge_commit.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec git config user.email "you@example.com" 3 | exec git config user.name "Your Name" 4 | exec git add -A 5 | exec git commit -m 'commit 1' 6 | exec lefthook install 7 | 8 | exec git checkout -b merge-me 9 | exec touch file.A 10 | exec git add -A 11 | exec git commit -m 'commit A' 12 | 13 | exec lefthook run pre-commit --force 14 | stdout 'skip-merge-commit' 15 | 16 | exec git checkout - 17 | exec touch file.B 18 | exec git add -A 19 | exec git commit -m 'commit B' 20 | 21 | exec git merge merge-me 22 | 23 | exec lefthook run pre-commit --force 24 | ! stdout 'skip-merge-commit' 25 | 26 | -- lefthook.yml -- 27 | skip_output: 28 | - skips 29 | - meta 30 | - summary 31 | - execution_info 32 | 33 | pre-commit: 34 | commands: 35 | skip-merge-commit: 36 | skip: 37 | - merge-commit 38 | run: echo 'skip-merge-commit' 39 | -------------------------------------------------------------------------------- /testdata/skip_run.txt: -------------------------------------------------------------------------------- 1 | [windows] skip 2 | 3 | exec git init 4 | exec git add -A 5 | exec lefthook run skip 6 | ! stdout 'Ha-ha!' 7 | exec lefthook run no-skip 8 | stdout 'Ha-ha!' 9 | 10 | exec lefthook run skip-var 11 | ! stdout 'Ha-ha!' 12 | 13 | env VAR=1 14 | exec lefthook run skip-var 15 | stdout 'Ha-ha!' 16 | 17 | -- lefthook.yml -- 18 | skip_output: 19 | - skips 20 | - meta 21 | - summary 22 | - execution_info 23 | skip: 24 | skip: 25 | - run: test 1 -eq 1 26 | commands: 27 | echo: 28 | run: echo 'Ha-ha!' 29 | 30 | no-skip: 31 | skip: 32 | - run: "[ 1 -eq 0 ]" 33 | commands: 34 | echo: 35 | run: echo 'Ha-ha!' 36 | 37 | skip-var: 38 | skip: 39 | - run: test -z $VAR 40 | commands: 41 | echo: 42 | run: echo 'Ha-ha!' 43 | -------------------------------------------------------------------------------- /testdata/skip_run_windows.txt: -------------------------------------------------------------------------------- 1 | [!windows] skip 2 | 3 | exec git init 4 | exec git add -A 5 | exec lefthook run skip 6 | ! stdout 'Ha-ha!' 7 | exec lefthook run no-skip 8 | stdout 'Ha-ha!' 9 | 10 | exec lefthook run skip-var 11 | ! stdout 'Ha-ha!' 12 | 13 | env VAR=1 14 | exec lefthook run skip-var 15 | stdout 'Ha-ha!' 16 | 17 | -- lefthook.yml -- 18 | skip_output: 19 | - skips 20 | - meta 21 | - summary 22 | - execution_info 23 | skip: 24 | skip: 25 | - run: if (1 -eq 1) { exit 0 } else { exit 1 } 26 | commands: 27 | echo: 28 | run: echo 'Ha-ha!' 29 | 30 | no-skip: 31 | skip: 32 | - run: if (1 -eq 0) { exit 0 } else { exit 1 } 33 | commands: 34 | echo: 35 | run: echo 'Ha-ha!' 36 | 37 | skip-var: 38 | skip: 39 | - run: if ([string]::IsNullOrEmpty($env:VAR)) { exit 0 } else { exit 1 } 40 | commands: 41 | echo: 42 | run: echo 'Ha-ha!' 43 | -------------------------------------------------------------------------------- /testdata/stage_fixed.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook install 3 | exec git config user.email "you@example.com" 4 | exec git config user.name "Your Name" 5 | exec git add -A 6 | exec git status --short 7 | exec git commit -m 'test stage_fixed' 8 | exec git status --short 9 | ! stdout . 10 | 11 | -- lefthook.yml -- 12 | min_version: 1.1.1 13 | pre-commit: 14 | commands: 15 | edit_file: 16 | run: | 17 | echo newline >> "[file].js" 18 | echo newline >> file.txt 19 | stage_fixed: true 20 | 21 | -- file.txt -- 22 | sometext 23 | 24 | -- [file].js -- 25 | somecode 26 | -------------------------------------------------------------------------------- /testdata/stage_fixed_505.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook install 3 | exec git config user.email "you@example.com" 4 | exec git config user.name "Your Name" 5 | exec git add -A 6 | exec git status --short 7 | exec git commit -m 'test stage_fixed' 8 | exec git status --short 9 | ! stdout . 10 | 11 | -- lefthook.yml -- 12 | pre-commit: 13 | commands: 14 | edit_file: 15 | run: echo "{staged_files}" && echo newline >> "[file].js" 16 | stage_fixed: true 17 | 18 | -- [file].js -- 19 | somecode 20 | -------------------------------------------------------------------------------- /testdata/templates.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook run test 3 | stdout '^\s*hello\s*$' 4 | 5 | -- lefthook.yml -- 6 | templates: 7 | message: hello 8 | 9 | output: 10 | - execution_out 11 | 12 | test: 13 | jobs: 14 | - run: echo {message} 15 | -------------------------------------------------------------------------------- /testdata/uninstall.txt: -------------------------------------------------------------------------------- 1 | exec git init 2 | exec lefthook install 3 | exists .git/hooks/pre-push 4 | exec lefthook uninstall 5 | ! exists .git/hooks-pre-push 6 | exists lefthook.yml 7 | exists .lefthook-local.toml 8 | 9 | exec lefthook install 10 | exists .git/hooks/pre-push 11 | exec lefthook uninstall -c 12 | ! exists .git/hooks-pre-push 13 | ! exists lefthook.yml 14 | ! exists .lefthook-local.toml 15 | 16 | -- lefthook.yml -- 17 | pre-push: 18 | commands: 19 | echo: 20 | run: echo pre-push 21 | 22 | 23 | -- .lefthook-local.toml -- 24 | [pre-commit.commands.echo] 25 | run = "echo pre-commit" 26 | -------------------------------------------------------------------------------- /testdata/version.txt: -------------------------------------------------------------------------------- 1 | exec lefthook version 2 | stdout \d+\.\d+\.\d+ 3 | ! stderr . 4 | --------------------------------------------------------------------------------