quick_check 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ea26165117cca41f73e40de4f2975ac21bfa9d86b3d097dabfe43fbf3147416
4
- data.tar.gz: 85d0174ff222df297448f18b3d6333b2cecc6aec6a82afacd86076568843f9f0
3
+ metadata.gz: e665ad565d365abca56c8521560fb97dd507683c8eec6d7c81084b2b393cf2d0
4
+ data.tar.gz: e82c198675cbc2aa956395d6b327088db5f5d4ad85f96a39c5abafb6830f57e4
5
5
  SHA512:
6
- metadata.gz: 4c4631164f1ce412d3ee1c3ee1d100197ea2311c3ea28d246a3edc3854466194670a4faabcfae939e44c97caeed2552ed1a5cc1e09f9b5ec46c0f307d82bfe1b
7
- data.tar.gz: 48b7538d2f0600a3381d8847eeab3455630612a715f0dbe70d996cd312fb9b7c15bd05083323eb9cc9f0eab788e1f40452dfa10332f623ef30ac9381c268d4e2
6
+ metadata.gz: 60f09a0569f3d80c90d95b41bc1d289cf136331bdf639e50d3c85931262488e9f6a5edc9678241b6ed1b9c570bfd8791031b8056acd1b51f7769cd85a3da3505
7
+ data.tar.gz: 2e38a08ee04981404e46b4099882a07927397aa66a9961704f480bb30b48cdb691af989e5c48cfb84d17475767e81309b8e3aed9801b616840b45ec2fbe9556b
data/README.md CHANGED
@@ -27,6 +27,7 @@ qc
27
27
  - Staged (index) changes
28
28
  - Untracked files (new specs not yet added)
29
29
  - All committed changes on your branch vs base (`main`/`master`)
30
+ - Renames and copies are tracked so moved tests still run
30
31
  - You can disable branch commits with `--no-committed`
31
32
  - Auto-detects base branch (`main` or `master`) or configure via `.quick_check.yml`
32
33
  - Auto-detects framework and command:
@@ -99,10 +100,10 @@ If no config is present, `qc` will use the first existing branch among `main` or
99
100
 
100
101
  ## How it works
101
102
 
102
- - Unstaged: `git diff --name-only --diff-filter=AM`
103
- - Staged: `git diff --name-only --cached --diff-filter=AM`
103
+ - Unstaged: `git diff --name-only -M -C --diff-filter=ACMR`
104
+ - Staged: `git diff --name-only --cached -M -C --diff-filter=ACMR`
104
105
  - Untracked: `git ls-files --others --exclude-standard`
105
- - Committed vs base: `git diff --name-only --diff-filter=AM <base>...HEAD`
106
+ - Committed vs base: `git diff --name-only -M -C --diff-filter=ACMR <base>...HEAD`
106
107
 
107
108
  Files are filtered to `spec/**/*_spec.rb` and/or `test/**/*_test.rb`, de-duplicated, sorted, and then:
108
109
 
@@ -17,7 +17,7 @@ module QuickCheck
17
17
  @argv = argv
18
18
  @options = {
19
19
  base_branch: nil,
20
- include_committed_diff: false,
20
+ include_committed_diff: true,
21
21
  include_staged: true,
22
22
  include_unstaged: true,
23
23
  custom_command: nil,
@@ -126,28 +126,72 @@ module QuickCheck
126
126
  files = []
127
127
 
128
128
  if @options[:include_unstaged]
129
- files.concat(git_diff_name_only(["--name-only", "--diff-filter=AM"]))
129
+ files.concat(git_diff_name_only(["--name-only", "-M", "-C", "--diff-filter=ACMR"]))
130
130
  files.concat(git_untracked_files)
131
131
  end
132
132
 
133
133
  if @options[:include_staged]
134
- files.concat(git_diff_name_only(["--name-only", "--cached", "--diff-filter=AM"]))
134
+ files.concat(git_diff_name_only(["--name-only", "--cached", "-M", "-C", "--diff-filter=ACMR"]))
135
135
  end
136
136
 
137
137
  if @options[:include_committed_diff]
138
138
  current_branch = git_current_branch
139
139
  if current_branch && base_branch && current_branch != base_branch
140
- # Include files changed on this branch vs base
141
- range = diff_range_against_base(base_branch)
142
- files.concat(git_diff_name_only(["--name-only", "--diff-filter=AM", range])) if range
140
+ upstream_branch = git_upstream_branch(current_branch)
141
+ # Prefer upstream tracking branch to correctly handle rebases (only shows local changes)
142
+ if upstream_branch && upstream_has_differences?(upstream_branch)
143
+ changed_files = diff_range_against_upstream(upstream_branch)
144
+ files.concat(changed_files) if changed_files
145
+ elsif !upstream_branch
146
+ # Fall back to base branch if no upstream exists
147
+ changed_files = diff_range_against_base(base_branch)
148
+ files.concat(changed_files) if changed_files
149
+ end
143
150
  end
144
151
  end
145
152
 
146
153
  files = files.compact.uniq
147
154
  rspec_specs = files.select { |f| f.match?(%r{\Aspec/.+_spec\.rb\z}) }
155
+ rspec_specs += infer_rspec_from_source(files)
148
156
  minitest_tests = files.select { |f| f.match?(%r{\Atest/.+_test\.rb\z}) }
149
157
 
150
- { rspec: rspec_specs.sort, minitest: minitest_tests.sort }
158
+ { rspec: rspec_specs.uniq, minitest: minitest_tests.uniq }
159
+ end
160
+
161
+ def infer_rspec_from_source(files)
162
+ candidates = []
163
+ files.each do |path|
164
+ next unless path.end_with?(".rb")
165
+
166
+ if path =~ %r{\Aapp/models/(.+)\.rb\z}
167
+ spec_path = File.join("spec", "models", "#{$1}_spec.rb")
168
+ candidates << spec_path if File.file?(spec_path)
169
+ next
170
+ end
171
+
172
+ if path =~ %r{\Aapp/controllers/(.+?)(?:_controller)?\.rb\z}
173
+ controller_path = Regexp.last_match(1)
174
+ req_base = File.join("spec", "requests", controller_path)
175
+ req_variants = [
176
+ "#{req_base}_spec.rb",
177
+ "#{req_base}_controller_spec.rb"
178
+ ].select { |p| File.file?(p) }
179
+ if req_variants.any?
180
+ candidates.concat(req_variants)
181
+ else
182
+ ctrl_spec = File.join("spec", "controllers", "#{controller_path}_controller_spec.rb")
183
+ candidates << ctrl_spec if File.file?(ctrl_spec)
184
+ end
185
+ next
186
+ end
187
+
188
+ if path =~ %r{\Alib/(.+)\.rb\z}
189
+ spec_path = File.join("spec", "lib", "#{$1}_spec.rb")
190
+ candidates << spec_path if File.file?(spec_path)
191
+ next
192
+ end
193
+ end
194
+ candidates
151
195
  end
152
196
 
153
197
  def ensure_git_repo!
@@ -196,12 +240,17 @@ module QuickCheck
196
240
  end
197
241
 
198
242
  def branch_exists?(name)
243
+ local_branch_exists?(name) || remote_branch_exists?(name)
244
+ end
245
+
246
+ def local_branch_exists?(name)
199
247
  ok, _out, _err = run_cmd(["git", "show-ref", "--verify", "--quiet", "refs/heads/#{name}"])
200
- return true if ok
248
+ ok
249
+ end
201
250
 
202
- # fall back to remote branch
203
- ok, _out, _err = run_cmd(["git", "ls-remote", "--heads", "origin", name])
204
- ok && !_out.to_s.strip.empty?
251
+ def remote_branch_exists?(name)
252
+ ok, out, _err = run_cmd(["git", "ls-remote", "--heads", "origin", name])
253
+ ok && !out.to_s.strip.empty?
205
254
  end
206
255
 
207
256
  def git_current_branch
@@ -214,13 +263,50 @@ module QuickCheck
214
263
  ok ? out.to_s.strip : nil
215
264
  end
216
265
 
217
- def diff_range_against_base(base)
218
- # Prefer the symmetric range base...HEAD if base exists
219
- if branch_exists?(base)
220
- "#{base}...HEAD"
221
- else
222
- nil
266
+ def git_upstream_branch(branch)
267
+ # Get the upstream tracking branch for the current branch
268
+ ok, out, _err = run_cmd(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "#{branch}@{u}"])
269
+ if ok && !out.to_s.strip.empty?
270
+ upstream = out.to_s.strip
271
+ # Verify the upstream branch actually exists
272
+ ok_check, _out_check, _err_check = run_cmd(["git", "rev-parse", "--verify", "--quiet", upstream])
273
+ return upstream if ok_check
223
274
  end
275
+ nil
276
+ rescue StandardError
277
+ nil
278
+ end
279
+
280
+ def upstream_has_differences?(upstream)
281
+ # Only use upstream if it has local commits (avoids testing already-pushed changes)
282
+ ok, out, _err = run_cmd(["git", "rev-list", "--count", "#{upstream}..HEAD"])
283
+ return false unless ok
284
+ out.to_s.strip.to_i > 0
285
+ end
286
+
287
+ def diff_range_against_upstream(upstream)
288
+ files = git_log_first_parent_files(upstream, "HEAD")
289
+ files.empty? ? nil : files
290
+ end
291
+
292
+ def diff_range_against_base(base)
293
+ base_ref = if local_branch_exists?(base)
294
+ base
295
+ elsif remote_branch_exists?(base)
296
+ "origin/#{base}"
297
+ else
298
+ return nil
299
+ end
300
+ files = git_log_first_parent_files(base_ref, "HEAD")
301
+ files.empty? ? nil : files
302
+ end
303
+
304
+ def git_log_first_parent_files(base_ref, head_ref)
305
+ # Use --first-parent to exclude merge commits, only showing changes from direct branch commits
306
+ cmd = ["git", "log", "--first-parent", "--name-only", "--diff-filter=ACMR", "--format=", "#{base_ref}..#{head_ref}"]
307
+ ok, out, _err = run_cmd(cmd)
308
+ return [] unless ok
309
+ out.split("\n").map(&:strip).reject(&:empty?).uniq
224
310
  end
225
311
 
226
312
  def git_diff_name_only(args)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module QuickCheck
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
@@ -53,7 +53,7 @@ RSpec.describe QuickCheck::CLI do
53
53
 
54
54
  it "runs rspec for changed rspec files" do
55
55
  stubs = base_git_stubs.merge(
56
- "git diff --name-only --diff-filter=ACMR" => { stdout: "spec/models/user_spec.rb\n" }
56
+ "git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "spec/models/user_spec.rb\n" }
57
57
  )
58
58
 
59
59
  status, out, _err = run_cli(["--dry-run"], git_outputs: stubs)
@@ -63,7 +63,7 @@ RSpec.describe QuickCheck::CLI do
63
63
 
64
64
  it "infers rspec from app source change" do
65
65
  stubs = base_git_stubs.merge(
66
- "git diff --name-only --diff-filter=ACMR" => { stdout: "app/models/user.rb\n" }
66
+ "git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "app/models/user.rb\n" }
67
67
  )
68
68
  status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: [
69
69
  File.join("spec", "models", "user_spec.rb")
@@ -74,7 +74,7 @@ RSpec.describe QuickCheck::CLI do
74
74
 
75
75
  it "maps controller to request spec with both base names" do
76
76
  stubs = base_git_stubs.merge(
77
- "git diff --name-only --diff-filter=ACMR" => { stdout: "app/controllers/account/users_controller.rb\n" }
77
+ "git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "app/controllers/account/users_controller.rb\n" }
78
78
  )
79
79
  existing = [
80
80
  File.join("spec", "requests", "account", "users_spec.rb"),
@@ -87,7 +87,7 @@ RSpec.describe QuickCheck::CLI do
87
87
 
88
88
  it "falls back to controller spec when no request spec exists" do
89
89
  stubs = base_git_stubs.merge(
90
- "git diff --name-only --diff-filter=ACMR" => { stdout: "app/controllers/home_controller.rb\n" }
90
+ "git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "app/controllers/home_controller.rb\n" }
91
91
  )
92
92
  existing = [File.join("spec", "controllers", "home_controller_spec.rb")]
93
93
  status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: existing)
@@ -97,7 +97,7 @@ RSpec.describe QuickCheck::CLI do
97
97
 
98
98
  it "runs minitest through rails test when test files change and rails present" do
99
99
  stubs = base_git_stubs.merge(
100
- "git diff --name-only --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
100
+ "git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
101
101
  )
102
102
  allow_any_instance_of(QuickCheck::CLI).to receive(:rails_available?).and_return(true)
103
103
  status, out, _err = run_cli(["--dry-run"], git_outputs: stubs, existing_files: [File.join("bin", "rails")])
@@ -107,11 +107,59 @@ RSpec.describe QuickCheck::CLI do
107
107
 
108
108
  it "runs per-file minitest when no rails" do
109
109
  stubs = base_git_stubs.merge(
110
- "git diff --name-only --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
110
+ "git diff --name-only -M -C --diff-filter=ACMR" => { stdout: "test/models/user_test.rb\n" }
111
111
  )
112
112
 
113
113
  status, out, _err = run_cli(["--dry-run"], git_outputs: stubs)
114
114
  expect(out.lines.map(&:strip)).to include("ruby -I test test/models/user_test.rb")
115
115
  expect(status).to eq(0)
116
116
  end
117
+
118
+ it "uses first-parent to handle rebases correctly" do
119
+ stubs = base_git_stubs.merge(
120
+ "git show-ref --verify --quiet refs/heads/main" => { success: true },
121
+ "git log --first-parent --name-only --diff-filter=ACMR --format= main..HEAD" => { stdout: "spec/my_feature_spec.rb\n" }
122
+ )
123
+
124
+ status, out, _err = run_cli(["--base", "main", "--dry-run", "--no-unstaged", "--no-staged"], git_outputs: stubs)
125
+ expect(status).to eq(0)
126
+ expect(out).to include("bundle exec rspec spec/my_feature_spec.rb")
127
+ end
128
+
129
+ it "prefers upstream branch over base branch when upstream has differences" do
130
+ stubs = base_git_stubs.merge(
131
+ "git rev-parse --abbrev-ref --symbolic-full-name feature@{u}" => { stdout: "origin/feature\n" },
132
+ "git rev-parse --verify --quiet origin/feature" => { success: true },
133
+ "git rev-list --count origin/feature..HEAD" => { stdout: "5\n" },
134
+ "git log --first-parent --name-only --diff-filter=ACMR --format= origin/feature..HEAD" => { stdout: "spec/my_local_change_spec.rb\n" }
135
+ )
136
+
137
+ status, out, _err = run_cli(["--base", "main", "--dry-run", "--no-unstaged", "--no-staged"], git_outputs: stubs)
138
+ expect(status).to eq(0)
139
+ expect(out).to include("bundle exec rspec spec/my_local_change_spec.rb")
140
+ end
141
+
142
+ it "does not include committed changes when upstream is in sync" do
143
+ stubs = base_git_stubs.merge(
144
+ "git rev-parse --abbrev-ref --symbolic-full-name feature@{u}" => { stdout: "origin/feature\n" },
145
+ "git rev-parse --verify --quiet origin/feature" => { success: true },
146
+ "git rev-list --count origin/feature..HEAD" => { stdout: "0\n" }
147
+ )
148
+
149
+ status, out, _err = run_cli(["--base", "main", "--dry-run", "--no-unstaged", "--no-staged"], git_outputs: stubs)
150
+ expect(status).to eq(0)
151
+ expect(out).to include("No changed/added test files detected.")
152
+ end
153
+
154
+ it "falls back to base branch when no upstream exists" do
155
+ stubs = base_git_stubs.merge(
156
+ "git rev-parse --abbrev-ref --symbolic-full-name feature@{u}" => { success: false, exitstatus: 128 },
157
+ "git show-ref --verify --quiet refs/heads/main" => { success: true },
158
+ "git log --first-parent --name-only --diff-filter=ACMR --format= main..HEAD" => { stdout: "spec/my_feature_spec.rb\n" }
159
+ )
160
+
161
+ status, out, _err = run_cli(["--base", "main", "--dry-run", "--no-unstaged", "--no-staged"], git_outputs: stubs)
162
+ expect(status).to eq(0)
163
+ expect(out).to include("bundle exec rspec spec/my_feature_spec.rb")
164
+ end
117
165
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quick_check
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasvit
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2025-08-27 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: Adds the `qc` command to run only changed or newly added RSpec files
14
13
  from uncommitted changes and vs base branch (main/master).
@@ -32,7 +31,6 @@ homepage: https://github.com/kasvit/quick_check
32
31
  licenses:
33
32
  - MIT
34
33
  metadata: {}
35
- post_install_message:
36
34
  rdoc_options: []
37
35
  require_paths:
38
36
  - lib
@@ -47,8 +45,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
45
  - !ruby/object:Gem::Version
48
46
  version: '0'
49
47
  requirements: []
50
- rubygems_version: 3.1.6
51
- signing_key:
48
+ rubygems_version: 3.6.9
52
49
  specification_version: 4
53
50
  summary: Run changed/added RSpec specs quickly
54
51
  test_files: []