diff --git a/.github/workflows/rubocop.yml b/.github/workflows/rubocop.yml index d442459..732cf2b 100644 --- a/.github/workflows/rubocop.yml +++ b/.github/workflows/rubocop.yml @@ -12,8 +12,6 @@ on: pull_request: # The branches below must be a subset of the branches above branches: [ main ] - schedule: - - cron: '20 4 * * 5' jobs: rubocop: diff --git a/.rubocop.yml b/.rubocop.yml index 72f47ae..f3d5561 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,6 @@ +require: + - rubocop-rake + AllCops: NewCops: enable TargetRubyVersion: 2.7 diff --git a/Gemfile b/Gemfile index 26ea26a..c57d2a7 100644 --- a/Gemfile +++ b/Gemfile @@ -8,3 +8,9 @@ group :ci do gem 'code-scanning-rubocop' gem 'rspec_junit_formatter' end + + +gem 'rake', '~> 13.0' +gem 'rspec' +gem 'rubocop', '~> 1.29' +gem 'rubocop-rake', '~> 0.6' diff --git a/Gemfile.lock b/Gemfile.lock index 677e689..9344382 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - format-staged (0.1.1) + format-staged (0.1.2) colorize GEM @@ -10,47 +10,51 @@ GEM ast (2.4.2) code-scanning-rubocop (0.6.1) rubocop (~> 1.0) - colorize (0.8.1) - diff-lcs (1.5.0) - parallel (1.22.1) - parser (3.1.2.0) + colorize (1.1.0) + diff-lcs (1.5.1) + json (2.7.2) + language_server-protocol (3.17.0.3) + parallel (1.25.1) + parser (3.3.4.0) ast (~> 2.4.1) + racc + racc (1.8.1) rainbow (3.1.1) - rake (13.0.6) - regexp_parser (2.4.0) - rexml (3.2.5) - rspec (3.11.0) - rspec-core (~> 3.11.0) - rspec-expectations (~> 3.11.0) - rspec-mocks (~> 3.11.0) - rspec-core (3.11.0) - rspec-support (~> 3.11.0) - rspec-expectations (3.11.0) + rake (13.2.1) + regexp_parser (2.9.2) + rexml (3.3.9) + rspec (3.13.0) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.0) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-mocks (3.11.1) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.11.0) - rspec-support (3.11.0) - rspec_junit_formatter (0.5.1) + rspec-support (~> 3.13.0) + rspec-support (3.13.1) + rspec_junit_formatter (0.6.0) rspec-core (>= 2, < 4, != 2.12.0) - rubocop (1.29.1) + rubocop (1.65.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 3.1.0.0) + parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) + regexp_parser (>= 2.4, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.17.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.18.0) - parser (>= 3.1.1.0) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.31.3) + parser (>= 3.3.1.0) rubocop-rake (0.6.0) rubocop (~> 1.0) - rubocop-rspec (2.11.1) - rubocop (~> 1.19) - ruby-progressbar (1.11.0) - unicode-display_width (2.1.0) + ruby-progressbar (1.13.0) + unicode-display_width (2.5.0) PLATFORMS ruby @@ -63,7 +67,6 @@ DEPENDENCIES rspec_junit_formatter rubocop (~> 1.29) rubocop-rake (~> 0.6) - rubocop-rspec BUNDLED WITH 2.1.4 diff --git a/README.md b/README.md index 6bfa56d..bcf923c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,109 @@ # git-format-staged -Port of [hallettj/git-format-staged](https://github.com/hallettj/git-format-staged) to Ruby. +Port of [hallettj/git-format-staged](https://github.com/hallettj/git-format-staged) +to Ruby. + +Consider a project where you want all code formatted consistently. So you use +a formatter and/or linter. (For example [SwiftFormat][]) You want to make sure +that everyone working on the project runs the formatter, add a git pre-commit +hook to run it. The naive way to write that hook would be to: + +- get a list of staged files +- run the formatter on those files +- run `git add` to stage the results of formatting + +The problem with that solution is it forces you to commit entire files. At +worst this will lead to contributors to unwittingly committing changes. At +best it disrupts workflow for contributors who use `git add -p`. + +git-format-staged tackles this problem by running the formatter on the staged +version of the file. Staging changes to a file actually produces a new file +that exists in the git object database. git-format-staged uses some git +plumbing commands to send content from that file to your formatter. The command +replaces file content in the git index. The process bypasses the working tree, +so any unstaged changes are ignored by the formatter, and remain unstaged. + +After formatting a staged file git-format-staged computes a patch which it +attempts to apply to the working tree file to keep the working tree in sync +with staged changes. If patching fails you will see a warning message. The +version of the file that is committed will be formatted properly - the warning +just means that working tree copy of the file has been left unformatted. The +patch step can be disabled with the `--no-update-working-tree` option. + +[SwiftFormat]: https://github.com/nicklockwood/SwiftFormat + +## How to install + +Requires Ruby 2.7 or newer. Tests run on 2.7 and 3.0. + +Install as a development dependency in a project that uses bundle to manage +Ruby dependencies: + + $ bundle add format-staged + +Or install globally: + + $ gem install format-staged + +## How to use + +For detailed information run: + + $ [bundle exec] git-format-staged --help + +The command expects a shell command to run a formatter, and one or more file +patterns to identify which files should be formatted. For example: + + $ git-format-staged --formatter 'prettier --stdin-filepath "{}"' '*.js' + +That will format all `.js` files using `prettier`. + +The formatter command must read file content from `stdin`, and output formatted +content to `stdout`. + +Patterns are evaluated from left-to-right: if a file matches multiple patterns +the right-most pattern determines whether the file is included or excluded. + +git-format-staged never operates on files that are excluded from version +control. So it is not necessary to explicitly exclude stuff like +`vendor/`. + +The formatter command may include a placeholder, `{}`, which will be replaced +with the path of the file that is being formatted. This is useful if your +formatter needs to know the file extension to determine how to format or to +lint each file. For example: + + $ git-format-staged -f 'prettier --stdin-filepath "{}"' '*.js' '*.css' + +Do not attempt to read or write to `{}` in your formatter command! The +placeholder exists only for referencing the file name and path. + +### Check staged changes with a linter without formatting + +Perhaps you do not want to reformat files automatically; but you do want to +prevent files from being committed if they do not conform to style rules. You +can use git-format-staged with the `--no-write` option, and supply a lint +command instead of a format command. Here is an example using ESLint: + + $ git-format-staged --no-write -f 'eslint --stdin --stdin-filename "{}" >&2' 'src/*.js' + +If this command is run in a pre-commit hook, and the lint command fails the +commit will be aborted and error messages will be displayed. The lint command +must read file content via `stdin`. Anything that the lint command outputs to +`stdout` will be ignored. In the example above `eslint` is given the `--stdin` +option to tell it to read content from `stdin` instead of reading files from +disk, and messages from `eslint` are redirected to `stderr` (using the `>&2` +notation) so that you can see them. + +### Why the Ruby port if there already is a fine Python implementation? + +I don’t like Python ;) + +But jokes aside, I am already setting up a Ruby environment (using [rbenv][]) for my +projects to run [cocoapods][] and [fastlane][] and our git hooks. By using this port +we don’t need to ensure to have python available as well. + + +[rbenv]: https://github.com/rbenv/rbenv/ +[cocoapods]: https://cocoapods.org/ +[fastlane]: https://fastlane.tools/ diff --git a/bin/git-format-staged b/bin/git-format-staged index 41d0829..2111bcc 100755 --- a/bin/git-format-staged +++ b/bin/git-format-staged @@ -17,7 +17,7 @@ parser = OptionParser.new do |opt| <<~DOC Shell command to format files, will run once per file. Occurrences of the placeholder `{}` will \ be replaced with a path to the file being formatted. \ - (Example: "prettier --stdin-filepath \'{}\'") + (Example: "prettier --stdin-filepath '{}'") DOC ) do |o| parameters[:formatter] = o diff --git a/format-staged.gemspec b/format-staged.gemspec index d8fd383..ddde6c8 100644 --- a/format-staged.gemspec +++ b/format-staged.gemspec @@ -19,12 +19,6 @@ Gem::Specification.new do |s| s.add_dependency 'colorize' - s.add_development_dependency 'rake', '~> 13.0' - s.add_development_dependency 'rspec' - s.add_development_dependency 'rubocop', '~> 1.29' - s.add_development_dependency 'rubocop-rake', '~> 0.6' - s.add_development_dependency 'rubocop-rspec' - s.metadata = { 'rubygems_mfa_required' => 'true' } diff --git a/lib/format-staged/job.rb b/lib/format-staged/job.rb index 18bacc8..176b099 100644 --- a/lib/format-staged/job.rb +++ b/lib/format-staged/job.rb @@ -39,7 +39,7 @@ module FormatStaged return false unless formatted.size == files.size quiet = @verbose ? [] : ['--quiet'] - return !write || get_status('git', 'diff-index', '--cached', '--exit-code', *quiet, 'HEAD') != 0 + !write || get_status('git', 'diff-index', '--cached', '--exit-code', *quiet, 'HEAD') != 0 end def repo_root diff --git a/lib/format-staged/version.rb b/lib/format-staged/version.rb index ff59012..d9e035e 100644 --- a/lib/format-staged/version.rb +++ b/lib/format-staged/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module FormatStaged - VERSION = '0.1.1' + VERSION = '0.1.2' end diff --git a/lib/format_staged.rb b/lib/format_staged.rb index d6e8bf2..683244f 100644 --- a/lib/format_staged.rb +++ b/lib/format_staged.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative 'format-staged/job' +require_relative 'format-staged/version' ## # FormatStaged module diff --git a/spec/test_spec.rb b/spec/test_spec.rb index ce53719..26fc0ff 100644 --- a/spec/test_spec.rb +++ b/spec/test_spec.rb @@ -132,7 +132,7 @@ describe FormatStaged do repo.set_content 'test.test', 'a=b' repo.stage 'test.test' - expect(repo.run_formatter write: false).to be_truthy + expect(repo.run_formatter(write: false)).to be_truthy expect(repo.get_content('test.test')).to eq 'a=b' expect(repo.get_staged('test.test')).to eq 'a=b' end