linter_changes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 51419ad25d4ed787015da7a0e24ba91cf7e5ccfbaedc7fa30fb444e60342ce34
4
+ data.tar.gz: 2cd1208664fcc6e82c499fb95808d1091181c6fddfe10f712c3d5cf9b9a51f19
5
+ SHA512:
6
+ metadata.gz: 41f118035e002bd8211a7678b09e888df1cd460878c39d17e09cca1a9ac956ed1f06f1f6c5092489a605c58dfe3eeb45cebd0f5eb80afd1a5651e1e92e9c9df9
7
+ data.tar.gz: 581696d673db1788efbc256b5d9018ada57ba134e2999574179a82e27c248fba23f5ced757640796f9c3edafc109324f1d707990fa5ac1628cece901fcf152bf
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # LinterChanges
2
+
3
+ LinterChanges is a Ruby gem that runs linters on files changed between your current branch and a target branch (e.g., `master`). It helps maintain code quality by ensuring that only the changed files are linted, saving time and resources.
4
+
5
+ **What sets LinterChanges apart from other tools like Pronto is that it checks entire files rather than just the changed lines when raising errors. Additionally, if configuration changes for the linter occur, LinterChanges will run the linter on the entire repository, not just on the current changes.**
6
+
7
+ Currently, **LinterChanges** supports **RuboCop** for Ruby code. Support for additional linters can be added in the future.
8
+
9
+ ---
10
+
11
+ ## Table of Contents
12
+
13
+ - [LinterChanges](#linterchanges)
14
+ - [Table of Contents](#table-of-contents)
15
+ - [Installation](#installation)
16
+ - [Usage](#usage)
17
+ - [Basic Usage](#basic-usage)
18
+ - [Specifying the Target Branch](#specifying-the-target-branch)
19
+ - [Customizing RuboCop Configuration](#customizing-rubocop-configuration)
20
+ - [Contributing](#contributing)
21
+ - [Running the test suite](#running-the-test-suite)
22
+ - [Acknowledgments](#acknowledgments)
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's `Gemfile`:
29
+
30
+ ```ruby
31
+ gem 'linter_changes', git: 'https://github.com/bukhr/linter_changes.git'
32
+ ```
33
+
34
+ ```bash
35
+ bundle install
36
+ ```
37
+ ---
38
+
39
+ ## Usage
40
+
41
+ LinterChanges provides a command-line interface (CLI) to run RuboCop on the files changed between your current branch and the target branch.
42
+
43
+ ### Basic Usage
44
+
45
+ By default, LinterChanges will:
46
+
47
+ - Compare your current branch with the `main` branch.
48
+ - Run the linters on the changed files that linter listen to.
49
+
50
+ **Command:**
51
+
52
+ ```bash
53
+ bin/linter_changes lint
54
+ ```
55
+
56
+ **Example Output:**
57
+
58
+ ```
59
+ Running RuboCop linter
60
+ Linting files with RuboCop: app/models/user.rb, app/controllers/users_controller.rb
61
+ Inspecting 2 files
62
+ ..
63
+
64
+ 2 files inspected, no offenses detected
65
+ ```
66
+
67
+ ### Specifying the Target Branch
68
+
69
+ If you want to compare against a different branch, you can specify it using the `--target-branch` option.
70
+
71
+ **Command:**
72
+
73
+ ```bash
74
+ bin/linter_changes lint --target-branch origin/master
75
+ ```
76
+
77
+ This will compare your current branch with the `origin/master` branch.
78
+
79
+ ### Customizing RuboCop Configuration
80
+
81
+ You can customize the RuboCop configuration files and command options.
82
+
83
+ **Specify Custom Config Files:**
84
+
85
+ Note: each config file passed is interpreted as regex on the full file path
86
+
87
+ ```bash
88
+ bin/linter_changes lint --config-files rubocop:rubocop,custom_rubocop.yml
89
+ ```
90
+
91
+ **Specify Custom RuboCop Command:**
92
+
93
+ ```bash
94
+ bin/linter_changes lint --linter-command rubocop:"rubocop --parallel"
95
+ ```
96
+
97
+ **Combining Both:**
98
+
99
+ ```bash
100
+ bin/linter_changes lint \
101
+ --config-files rubocop:.rubocop.yml,custom_rubocop.yml \
102
+ --linter-command rubocop:"rubocop --parallel"
103
+ ```
104
+
105
+ ---
106
+
107
+ ## Contributing
108
+
109
+ Contributions are welcome! If you'd like to contribute, please follow these steps:
110
+
111
+ 1. Fork the repository.
112
+ 2. Create a new branch:
113
+
114
+ ```bash
115
+ git checkout -b feature/your_feature_name
116
+ ```
117
+
118
+ 3. Make your changes.
119
+ 4. Commit your changes:
120
+
121
+ ```bash
122
+ git commit -m "Add your commit message"
123
+ ```
124
+
125
+ 5. Push to your branch:
126
+
127
+ ```bash
128
+ git push origin feature/your_feature_name
129
+ ```
130
+
131
+ 6. Open a pull request on GitHub.
132
+
133
+ ---
134
+
135
+ ## Running the test suite
136
+
137
+ ```bash
138
+ bundle exec rake test
139
+ ```
140
+
141
+ ---
142
+
143
+ ## Acknowledgments
144
+
145
+ - [RuboCop](https://github.com/rubocop/rubocop) - The Ruby static code analyzer and formatter.
146
+ - [Thor](https://github.com/erikhuda/thor) - A toolkit for building powerful command-line interfaces.
147
+
148
+ ---
149
+
150
+ **Note:** Currently, LinterChanges supports only RuboCop for linting Ruby files. Support for additional linters may be added in the future.
151
+
152
+ **Key Features:**
153
+
154
+ - **Full File Linting:** Unlike tools like Pronto that only check the changed lines, LinterChanges lints the entire files that have been modified. This ensures that any issues in the modified files are caught, not just those in the changed lines.
155
+ - **Configuration Change Detection:** If a configuration file for the linter (e.g., `.rubocop.yml`) has changed, LinterChanges will run the linter on the entire repository. This ensures that any new or altered linting rules are applied across all files.
156
+
157
+ ---
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require_relative '../lib/cli'
5
+
6
+ LinterChanges::CLI.start(ARGV)
data/bin/setup ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
data/lib/cli.rb ADDED
@@ -0,0 +1,89 @@
1
+ # lib/linter_changes/cli.rb
2
+
3
+ require 'bundler/setup'
4
+ require 'thor'
5
+ require_relative 'git_diff'
6
+ require_relative 'logger'
7
+ require_relative 'linter/base'
8
+ require_relative 'linter/rubocop/adapter'
9
+ require 'pry-byebug'
10
+
11
+ module LinterChanges
12
+ class CLI < Thor
13
+ class_option :debug, type: :boolean, default: false, desc: 'Enable debug mode'
14
+ class_option :target_branch, type: :string, default: nil, desc: 'Specify the target branch'
15
+
16
+ desc 'lint', 'Run linters on changed files'
17
+ method_option :linters, type: :array, default: [], desc: 'Specify linters to run (e.g., rubocop,eslint)'
18
+ method_option :config_files, type: :hash, default: {},
19
+ desc: 'Specify config files per linter (e.g., rubocop=.rubocop.yml)'
20
+ method_option :linter_command, type: :hash, default: {},
21
+ desc: 'Specify command per linter (e.g., rubocop="rubocop --parallel")'
22
+ def lint
23
+ Logger.debug_mode = options[:debug]
24
+
25
+ git_diff = GitDiff.new(target_branch: options[:target_branch])
26
+ changed_files = git_diff.changed_files
27
+ linters = select_linters git_diff
28
+ overall_success = true
29
+
30
+ linters.each do |linter|
31
+ Logger.debug "Running #{linter.name.capitalize} linter"
32
+ if linter.config_changed?(changed_files)
33
+ Logger.debug "#{linter.name.capitalize} configuration changed. Running linter globally."
34
+ success = linter.run
35
+ else
36
+ files_to_lint = linter.list_target_files & changed_files
37
+ if files_to_lint.empty?
38
+ Logger.debug "No files to lint for #{linter.name.capitalize}."
39
+ next
40
+ else
41
+ Logger.debug "Linting files with #{linter.name.capitalize}: #{files_to_lint.join(', ')}"
42
+ success = linter.run(files: files_to_lint)
43
+ end
44
+ end
45
+
46
+ overall_success &&= success
47
+ end
48
+
49
+ exit(overall_success ? 0 : 1)
50
+ end
51
+
52
+ def self.exit_on_failure?
53
+ true
54
+ end
55
+
56
+ no_commands do
57
+ def select_linters git_diff
58
+ linter_names = if options[:linters].empty?
59
+ AVAILABLE_LINTERS.keys
60
+ else
61
+ options[:linters]
62
+ end
63
+ linter_names.map do |name|
64
+ klass = AVAILABLE_LINTERS[name]
65
+ unless klass
66
+ Logger.error "Unknown linter specified: #{name}"
67
+ exit 1
68
+ end
69
+
70
+ # Pass custom config files and commands if provided
71
+ config_files = parse_config_files_option(name)
72
+ command = options[:linter_command][name]
73
+ klass.new(config_files:, command:, git_diff:)
74
+ end
75
+ end
76
+
77
+ def parse_config_files_option(linter_name)
78
+ config_files_option = options[:config_files][linter_name]
79
+ return nil unless config_files_option
80
+
81
+ config_files_option.split(',')
82
+ end
83
+ end
84
+
85
+ AVAILABLE_LINTERS = {
86
+ 'rubocop' => LinterChanges::Linter::RuboCop::Adapter
87
+ }.freeze
88
+ end
89
+ end
data/lib/git_diff.rb ADDED
@@ -0,0 +1,39 @@
1
+ # lib/your_linter_gem/git_diff.rb
2
+
3
+ require 'open3'
4
+
5
+ module LinterChanges
6
+ class GitDiff
7
+ DEFAULT_TARGET_BRANCH = 'origin/master'
8
+
9
+ def initialize(target_branch: nil)
10
+ @target_branch = target_branch || ENV['CHANGE_TARGET'] || DEFAULT_TARGET_BRANCH
11
+ Logger.debug "Target branch: #{@target_branch}"
12
+ end
13
+
14
+ def changed_files
15
+ cmd = "git diff --name-only #{@target_branch}...HEAD"
16
+ Logger.debug "Executing command: #{cmd}"
17
+
18
+ stdout, stderr, status = Open3.capture3(cmd)
19
+ unless status.success?
20
+ Logger.error "Error obtaining git changes: #{stderr}"
21
+ exit 1
22
+ end
23
+
24
+ files = stdout.strip.split("\n")
25
+ Logger.debug "Changed files: #{files.join(', ')}"
26
+ files
27
+ end
28
+
29
+ def changed_lines_contains? file:, pattern:
30
+ cmd = "git diff #{@target_branch}...HEAD -- #{file}"
31
+ stdout, stderr, status = Open3.capture3(cmd)
32
+ unless status.success?
33
+ Logger.error "Error obtaining git diff for #{file}: #{stderr}"
34
+ exit 1
35
+ end
36
+ stdout.include? pattern
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,46 @@
1
+ # lib/linter_changes/linter/base.rb
2
+
3
+ module LinterChanges
4
+ module Linter
5
+ class Base
6
+ attr_reader :name
7
+
8
+ def initialize(config_files: nil, command: nil, git_diff: nil)
9
+ @name = self.class.name.split('::')[-2].downcase
10
+ default_config_file = YAML.load_file("lib/linter/#{@name}/default_config.yml")
11
+ @config_files = config_files || default_config_file['config_files']
12
+ @command = command || default_config_file['command']
13
+ @git_diff = git_diff
14
+ end
15
+
16
+ # Returns an array of files that the linter targets
17
+ def list_target_files
18
+ raise NotImplementedError, "#{self.class} must implement #list_target_files"
19
+ end
20
+
21
+ # Checks if any configuration files have changed
22
+ def config_changed?(changed_files)
23
+ changed = @config_files.any? do |pattern|
24
+ changed_files.any? { |file| file.match? Regexp.new(pattern) }
25
+ end
26
+ Logger.debug "#{@name.capitalize} configuration changed: #{changed}"
27
+ changed
28
+ end
29
+
30
+ # Runs the linter on the specified files
31
+ def run(files: [])
32
+ raise NotImplementedError, "#{self.class} must implement #run"
33
+ end
34
+
35
+ private
36
+
37
+ def default_config_files
38
+ raise NotImplementedError, "#{self.class} must implement #default_config_files"
39
+ end
40
+
41
+ def default_command
42
+ raise NotImplementedError, "#{self.class} must implement #default_command"
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,64 @@
1
+ # lib/linter_changes/linter/rubocop/adapter.rb
2
+
3
+ module LinterChanges
4
+ module Linter
5
+ module RuboCop
6
+ class Adapter < Base
7
+ # By default, anything containing rubocop on the file path will be consider a config file
8
+ DEFAULT_CONFIG_FILES = [/rubocop/].freeze
9
+ DEFAULT_COMMAND = 'bin/rubocop'.freeze
10
+
11
+ def list_target_files
12
+ cmd = "#{DEFAULT_COMMAND} --list-target-files"
13
+ Logger.debug "Executing command: #{cmd}"
14
+
15
+ stdout, stderr, status = Open3.capture3(cmd)
16
+ unless status.success?
17
+ Logger.error "Error listing RuboCop target files: #{stderr}"
18
+ exit 1
19
+ end
20
+
21
+ stdout.strip.split("\n")
22
+ end
23
+
24
+ def run(files: [])
25
+ cmd = if files.empty?
26
+ @command
27
+ else
28
+ "#{@command} #{files.join(' ')}"
29
+ end
30
+ execute_linter(cmd)
31
+ end
32
+
33
+ def config_changed?(changed_files)
34
+ # Check if any of the config files have changed
35
+ return true if super
36
+
37
+ # TODO: get the location of Gemfile.lock with bundle command
38
+ # Check if Gemfile.lock has changed and contains something related to rubocop
39
+ if changed_files.include?('Gemfile.lock') && @git_diff.changed_lines_contains?(file: 'Gemfile.lock',
40
+ pattern: 'rubocop')
41
+ Logger.debug 'Something related to rubocop gem version changed in Gemfile.lock'
42
+ return true
43
+ end
44
+ false
45
+ end
46
+
47
+ private
48
+
49
+ def default_config_files
50
+ DEFAULT_CONFIG_FILES
51
+ end
52
+
53
+ def default_command
54
+ DEFAULT_COMMAND
55
+ end
56
+
57
+ def execute_linter(command)
58
+ Logger.debug "Executing RuboCop command: #{command}"
59
+ system(command)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ ---
2
+ # Only regex supported
3
+ config_files:
4
+ - rubocop
5
+ command: bin/rubocop
data/lib/logger.rb ADDED
@@ -0,0 +1,17 @@
1
+ # lib/your_linter_gem/logger.rb
2
+
3
+ module LinterChanges
4
+ class Logger
5
+ class << self
6
+ attr_accessor :debug_mode
7
+
8
+ def debug(message)
9
+ puts "[DEBUG] #{message}" if debug_mode
10
+ end
11
+
12
+ def error(message)
13
+ puts "[ERROR] #{message}"
14
+ end
15
+ end
16
+ end
17
+ end
data/lib/version.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LinterChanges
4
+ VERSION = '0.1.0'
5
+ end
metadata ADDED
@@ -0,0 +1,232 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: linter_changes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jose Lara
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-11-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '5.0'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: open3
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: thor
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '1.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: activesupport
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '7'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '7'
75
+ - !ruby/object:Gem::Dependency
76
+ name: minitest
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 5.24.1
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 5.24.1
89
+ - !ruby/object:Gem::Dependency
90
+ name: mocha
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: pry-byebug
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: rake
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: rexml
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ - !ruby/object:Gem::Dependency
146
+ name: rubocop
147
+ requirement: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - "~>"
150
+ - !ruby/object:Gem::Version
151
+ version: 1.63.4
152
+ type: :development
153
+ prerelease: false
154
+ version_requirements: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: 1.63.4
159
+ - !ruby/object:Gem::Dependency
160
+ name: shoulda-context
161
+ requirement: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: 3.0.0.rc1
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: 3.0.0.rc1
173
+ - !ruby/object:Gem::Dependency
174
+ name: shoulda-matchers
175
+ requirement: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '6.0'
180
+ type: :development
181
+ prerelease: false
182
+ version_requirements: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - "~>"
185
+ - !ruby/object:Gem::Version
186
+ version: '6.0'
187
+ description:
188
+ email:
189
+ - jvlara@uc.cl
190
+ executables:
191
+ - linter_changes
192
+ extensions: []
193
+ extra_rdoc_files: []
194
+ files:
195
+ - README.md
196
+ - bin/linter_changes
197
+ - bin/setup
198
+ - lib/cli.rb
199
+ - lib/git_diff.rb
200
+ - lib/linter/base.rb
201
+ - lib/linter/rubocop/adapter.rb
202
+ - lib/linter/rubocop/default_config.yml
203
+ - lib/logger.rb
204
+ - lib/version.rb
205
+ homepage: https://github.com/bukhr/linter_changes
206
+ licenses:
207
+ - MIT
208
+ metadata: {}
209
+ post_install_message:
210
+ rdoc_options: []
211
+ require_paths:
212
+ - lib
213
+ required_ruby_version: !ruby/object:Gem::Requirement
214
+ requirements:
215
+ - - ">="
216
+ - !ruby/object:Gem::Version
217
+ version: '3.0'
218
+ - - "<"
219
+ - !ruby/object:Gem::Version
220
+ version: '4.0'
221
+ required_rubygems_version: !ruby/object:Gem::Requirement
222
+ requirements:
223
+ - - ">="
224
+ - !ruby/object:Gem::Version
225
+ version: '0'
226
+ requirements: []
227
+ rubygems_version: 3.4.10
228
+ signing_key:
229
+ specification_version: 4
230
+ summary: Runs linters on code changes based on Git, either globally or only on modified
231
+ files, depending on the changes.
232
+ test_files: []