specguard-ruby 0.3.8 → 0.3.9

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: df7e70603cb326ea7db7783c95263a418f42b2a2472c81af5013942e31e4a936
4
- data.tar.gz: e586dcea9b754ec7f953c4cbf9c24f661fc410916551cca16e5cfc39a80941b9
3
+ metadata.gz: 6266c2bcd8775838ab93bbb3963cb007465575ee038f67490161fa64e10e982e
4
+ data.tar.gz: 7799e565338f59d3a76b4b19e19a0877dd172072e772e8cc81909f5d040d3ec7
5
5
  SHA512:
6
- metadata.gz: 7e5ca1fac05f5891a127a59bf052b2c7840738880a6d2f94c0aaa608cfb5f967af5aec59910c340af09c19fae22a791d12941071ba826d93a8cf04c171380e4a
7
- data.tar.gz: 5b91217eb10b3552f8c83a3890a3d2fc69e2740f0572f20f4980c9b372dd2d6e13a35575609b030947db53cd86f71a384b9d83c66adc6d04b311b5f422dbbe35
6
+ metadata.gz: 3e811640885a89c54dec7bd69702ca429b0f28f5bfdd7c76fd049d15621da4ce0b48323af914448236877f9bc40df78df642232cda89dbfef5a0adcd85612ed7
7
+ data.tar.gz: 28e11252d3051943734247a9878a50680ee719cff789a1ea53da11041574529ed5d9e3761c5445f33b84b0ae42994ae121d35895ec6c0f33fd2c8045163c329e
data/README.md CHANGED
@@ -54,6 +54,22 @@ bundle exec specguard-lint --changed # CI mode: only files in the current diff
54
54
  bundle exec specguard-lint # one-off audit: every *_spec.rb
55
55
  ```
56
56
 
57
+ `--changed` is the CI selection mode: it diffs against the **merge base with
58
+ the default branch** (`origin/HEAD`, then `origin/main`/`origin/master`, then
59
+ their local names) — never a bare working-tree-vs-index `git diff`, which is
60
+ empty on a clean checkout. `--changed=<base>` overrides the base for pipelines
61
+ that know better. In a **shallow** checkout — the default depth-1 `git clone`
62
+ behind `actions/checkout@v4` — the merge base with the default branch is not
63
+ in the clone's history, so the derived base is HEAD itself and the selection
64
+ comes up empty: it still exits `0`, but its stderr note (and `--json`
65
+ `selection.note`) names the checkout as **shallow** and the remedy — fetch the
66
+ default branch (`fetch-depth: 0`) or pass `--changed=<base>` naming a base the
67
+ checkout contains. An explicit `--changed=<base>` that is not in a shallow
68
+ checkout's history is a usage error (exit `2`) naming that cause and the same
69
+ remedy, not the bare "could not diff against" a full clone reports for a
70
+ genuinely bad ref. A selection that comes up empty stays exit `0` and says WHY
71
+ on stderr — the exit contract is unchanged; only the WHY is truthful now.
72
+
57
73
  Files are **positional** (`specguard-lint spec/order_spec.rb`); there is no `--source` flag —
58
74
  that belongs to `validate-intent`, not to this one.
59
75
 
@@ -43,6 +43,16 @@ module SpecGuard
43
43
  # There is no diff base that makes "what changed on this build" non-empty
44
44
  # there.
45
45
  #
46
+ # A **shallow** checkout — the depth-1 clone CI defaults to
47
+ # (`actions/checkout@v4`) — reaches the same thin base by a third road the
48
+ # ref-resolution above cannot see: every `DEFAULT_BRANCH_REFS` probe
49
+ # *resolves* (the fetched tip is in the clone), but the history behind it
50
+ # is not, so the merge base with the default branch is outside the clone
51
+ # and `merge-base` collapses onto HEAD. The notes below therefore consult
52
+ # `git rev-parse --is-shallow-repository` — lazily and memoized, only
53
+ # where the answer changes the message — and in a shallow clone say that,
54
+ # never "default-branch build".
55
+ #
46
56
  # That is why the load-bearing requirement is the **loud empty selection**,
47
57
  # not the base. The caller must never be unable to tell "checked 12 files,
48
58
  # found no annotations" from "checked 0 files" — {Selection} carries the
@@ -157,17 +167,21 @@ module SpecGuard
157
167
  "and no HEAD commit); pass --changed=<base> explicitly"
158
168
  end
159
169
 
160
- files, stats = changed_files(resolved, root)
170
+ # One memoized probe per run, shared by the two message branches that
171
+ # consult it (a failed diff, a thin base note).
172
+ is_shallow = shallow_probe(root)
173
+
174
+ files, stats = changed_files(resolved, root, is_shallow)
161
175
 
162
176
  Selection.new(files: files, mode: :changed, base: resolved,
163
- note: base_note(resolved, base_kind, root), stats: stats)
177
+ note: base_note(resolved, base_kind, root, is_shallow), stats: stats)
164
178
  end
165
179
 
166
180
  # Resolves git's repo-root-relative output into paths relative to `root`,
167
181
  # dropping (and counting) everything the two scoping rules exclude.
168
182
  # @return [[Array<String>, Stats]]
169
- def changed_files(base, root)
170
- names = diff_names(base, root)
183
+ def changed_files(base, root, is_shallow)
184
+ names = diff_names(base, root, is_shallow)
171
185
  specs = names.select { |name| CHANGED_PATTERNS.any? { |pattern| File.fnmatch?(pattern, name) } }
172
186
 
173
187
  top = toplevel(root)
@@ -198,9 +212,23 @@ module SpecGuard
198
212
  # them, and they then fail to open. `-z` makes the output machine-readable:
199
213
  # NUL-separated and never `core.quotePath`-quoted, so a non-ASCII path
200
214
  # survives intact and a path containing a newline cannot split a record.
201
- def diff_names(base, root)
215
+ def diff_names(base, root, is_shallow)
202
216
  out, ok = git(%W[diff -z --name-only --diff-filter=d #{base} --], root)
203
- raise UsageError, "--changed could not diff against #{base.inspect}" unless ok
217
+ unless ok
218
+ if is_shallow.call
219
+ # SPGD-1035: in a shallow checkout the overwhelmingly likely cause
220
+ # is that <base> is simply not in the clone's history (git's own
221
+ # stderr says `fatal: bad object` / `unknown revision`), and the
222
+ # fix is a fetch — a cause the bare message never named, leaving
223
+ # "bad ref" and "shallow history" indistinguishable. Non-shallow
224
+ # failures keep the original message.
225
+ raise UsageError,
226
+ "--changed could not diff against #{base.inspect}: this checkout is shallow and " \
227
+ "#{base.inspect} is not in its history — fetch it (fetch-depth: 0, or " \
228
+ "git fetch origin #{base}) or pass a base the checkout contains"
229
+ end
230
+ raise UsageError, "--changed could not diff against #{base.inspect}"
231
+ end
204
232
 
205
233
  out.split("\0").reject(&:empty?)
206
234
  end
@@ -241,20 +269,60 @@ module SpecGuard
241
269
  # `git diff HEAD`, i.e. the working-tree-vs-HEAD no-op this class exists
242
270
  # to avoid. Reporting the first when the second is true would be a
243
271
  # confidently wrong explanation.
244
- def base_note(resolved, base_kind, root)
272
+ #
273
+ # SPGD-1035: a shallow (depth-limited) checkout reproduces both thin
274
+ # shapes with a third cause those two stories miss — the merge base with
275
+ # the default branch is not in the clone's history at all — so whenever
276
+ # the repo IS shallow the note tells that story and names the remedy
277
+ # (fetch the default branch, or pass a base the checkout contains),
278
+ # retiring the "default-branch build" guess for shallow repos in both
279
+ # shapes.
280
+ def base_note(resolved, base_kind, root, is_shallow)
281
+ shallow_remedy =
282
+ "fetch the default branch (fetch-depth: 0) or pass --changed=<base> naming a base this checkout contains"
245
283
  case base_kind
246
284
  when :head_fallback
285
+ if is_shallow.call
286
+ return "this checkout is a shallow (depth-limited) clone and no default-branch ref " \
287
+ "(#{DEFAULT_BRANCH_REFS.join(', ')}) is in its history, so the diff base fell " \
288
+ "back to HEAD; --changed can only select uncommitted changes here — #{shallow_remedy}"
289
+ end
290
+
247
291
  "no default-branch ref (#{DEFAULT_BRANCH_REFS.join(', ')}) could be found, so the diff base " \
248
292
  "fell back to HEAD; --changed can only select uncommitted changes here"
249
293
  when :merge_base
250
294
  head, ok = git(%w[rev-parse HEAD], root)
251
295
  return nil unless ok && head.strip == resolved
252
296
 
297
+ if is_shallow.call
298
+ return "this checkout is a shallow (depth-limited) clone, so the merge base with the " \
299
+ "default branch is not in its history and the diff base is HEAD itself — only " \
300
+ "uncommitted changes can be selected; #{shallow_remedy}"
301
+ end
302
+
253
303
  "the diff base is HEAD itself (this looks like a default-branch build), " \
254
304
  "so only uncommitted changes can be selected"
255
305
  end
256
306
  end
257
307
 
308
+ # Memoized per-run `git rev-parse --is-shallow-repository` probe.
309
+ # SPGD-1035: shallowness is consulted only in the branches where it
310
+ # changes the message (a derived base of HEAD, a failed diff), so the
311
+ # full-clone happy path makes zero new git invocations and the memo caps
312
+ # the probe at one per run. An unreadable answer (old git without the
313
+ # flag) reads as not shallow, keeping today's messages exactly.
314
+ # @return [Proc] zero-argument; true iff git reports a shallow repository
315
+ def shallow_probe(root)
316
+ cached = nil
317
+ lambda do
318
+ if cached.nil?
319
+ out, ok = git(%w[rev-parse --is-shallow-repository], root)
320
+ cached = ok && out.strip == "true"
321
+ end
322
+ cached
323
+ end
324
+ end
325
+
258
326
  def git_repository?(root)
259
327
  out, ok = git(%w[rev-parse --is-inside-work-tree], root)
260
328
  ok && out.strip == "true"
@@ -7,5 +7,5 @@
7
7
  # framework-scoped version constant a lie, so the number moved here and the
8
8
  # rspec module now points at it for back-compat.
9
9
  module SpecGuard
10
- VERSION = "0.3.8"
10
+ VERSION = "0.3.9"
11
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: specguard-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 0.3.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - specguard Agent
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-09 00:00:00.000000000 Z
11
+ date: 2026-09-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json