kettle-family 1.2.17 → 1.2.18
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 +4 -4
- checksums.yaml.gz.sig +1 -4
- data/CHANGELOG.md +18 -1
- data/README.md +1 -1
- data/lib/kettle/family/report.rb +21 -0
- data/lib/kettle/family/version.rb +1 -1
- data/lib/kettle/family/workflow.rb +102 -11
- data.tar.gz.sig +0 -0
- metadata +10 -10
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e868e193e146eeb559019b4f24a860b25ccacee6f86a6f4b000fd2cdc101217d
|
|
4
|
+
data.tar.gz: c3dc9a386dde8a4d6ff1ee6918c966041d47b5316f96510b9c664c36a808fab5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d4665f48921ce7e0d339c17ea305790da6dd2ee4f5739df885535f99467a52db9ead2090cfca4e6c927e5b35106177f9642f97fdb86d77d06b872715773a441f
|
|
7
|
+
data.tar.gz: 43565531dce057aebe8f8bdc441f0d90cf33940dc13278eda9a55c0c5a02aa1a5aeb809a75fecc9b26131e12b294ab13be4fe69fbc90e19b48777911f2bf3b23
|
checksums.yaml.gz.sig
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
���Y�`�U�����NN����fX*��&�m�n��6�&[[Q����r�==�<T�)�e�/�R�?�E�*���\����8�11��!E�P`���nF���<�ړ�
|
|
3
|
-
���*{�?�v�ݗ���V�B6�L
|
|
4
|
-
�}s瘢,:�i2cr���l�P-�gB�j+��]��T�K�0�R��j��I}]v+X`L�&6/3HTPq�{H��R!"���]��]WM)ǽ^i�/g���R�O{/-�����Ҕ����k�ŀ(��L\ID����|X��6#R�w9�����-���Pܗ^Y� ���x��O�
|
|
1
|
+
׳�_�/4�:����s'e�s�$%�Ψ��a��_n�fK1;����|�S3?E�1�2�W9O9J�ak�\O�"��L:��P�ĸ��8́"��z�*)�6�r����p��Ndg����7Fb��%>ʹ���٢Uw�2���h.�Ne��vi�4�V�M���2�L�Hp
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,21 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.2.18] - 2026-07-31
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.2.18][1.2.18t]
|
|
36
|
+
- COVERAGE: 94.14% -- 4277/4543 lines in 28 files
|
|
37
|
+
- BRANCH COVERAGE: 76.75% -- 1680/2189 branches in 28 files
|
|
38
|
+
- 28.88% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Family template progress and final reports now aggregate checksum write
|
|
43
|
+
bypasses, rendered-but-unchanged files, and changed files from Kettle-Jem
|
|
44
|
+
NDJSON summaries.
|
|
45
|
+
- Template summaries now distinguish exact checksum hits from source-matched
|
|
46
|
+
local destination changes protected by the template-only checksum policy.
|
|
47
|
+
|
|
33
48
|
## [1.2.17] - 2026-07-31
|
|
34
49
|
|
|
35
50
|
- TAG: [v1.2.17][1.2.17t]
|
|
@@ -1540,7 +1555,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
1540
1555
|
- Fixed CI load failures on engines without compatible `pty` support by falling back to Open3 for interactive release commands.
|
|
1541
1556
|
- Fixed Ruby 3.2 version-bump support by loading Prism lazily and wiring the Prism gem only for MRI versions that need it.
|
|
1542
1557
|
|
|
1543
|
-
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.
|
|
1558
|
+
[Unreleased]: https://github.com/kettle-dev/kettle-family/compare/v1.2.18...HEAD
|
|
1559
|
+
[1.2.18]: https://github.com/kettle-dev/kettle-family/compare/v1.2.17...v1.2.18
|
|
1560
|
+
[1.2.18t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.18
|
|
1544
1561
|
[1.2.17]: https://github.com/kettle-dev/kettle-family/compare/v1.2.16...v1.2.17
|
|
1545
1562
|
[1.2.17t]: https://github.com/kettle-dev/kettle-family/releases/tag/v1.2.17
|
|
1546
1563
|
[1.2.16]: https://github.com/kettle-dev/kettle-family/compare/v1.2.15...v1.2.16
|
data/README.md
CHANGED
|
@@ -819,7 +819,7 @@ Thanks for RTFM. ☺️
|
|
|
819
819
|
[📌gitmoji]: https://gitmoji.dev
|
|
820
820
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
821
821
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
822
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.
|
|
822
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-4.543-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
823
823
|
[🔐security]: https://github.com/kettle-dev/kettle-family/blob/main/SECURITY.md
|
|
824
824
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
825
825
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/lib/kettle/family/report.rb
CHANGED
|
@@ -309,8 +309,15 @@ module Kettle
|
|
|
309
309
|
return if template_results.empty?
|
|
310
310
|
|
|
311
311
|
changed_files = template_results.sum { |result| template_changed_file_count(result) }
|
|
312
|
+
outcome_counts = template_results.map { |result| template_file_outcomes(result) }
|
|
312
313
|
lines << "template summary:"
|
|
313
314
|
lines << " #{template_results.count(&:ok?)}/#{template_results.length} members ok"
|
|
315
|
+
if outcome_counts.all?
|
|
316
|
+
lines << " #{outcome_counts.sum { |outcomes| outcomes.fetch(:checksum_hits) }} checksum hits"
|
|
317
|
+
protected = outcome_counts.sum { |outcomes| outcomes.fetch(:checksum_protected) }
|
|
318
|
+
lines << " #{protected} checksum-protected changes" if protected.positive?
|
|
319
|
+
lines << " #{outcome_counts.sum { |outcomes| outcomes.fetch(:unchanged) }} unchanged"
|
|
320
|
+
end
|
|
314
321
|
lines << " #{changed_files} file#{"s" unless changed_files == 1} changed"
|
|
315
322
|
end
|
|
316
323
|
|
|
@@ -339,6 +346,20 @@ module Kettle
|
|
|
339
346
|
summary&.fetch("changed_count")&.to_i
|
|
340
347
|
end
|
|
341
348
|
|
|
349
|
+
def template_file_outcomes(result)
|
|
350
|
+
summary = template_events(result.stdout).reverse.find do |event|
|
|
351
|
+
event["type"] == "summary" && event.key?("checksum_hit_count") &&
|
|
352
|
+
event.key?("checksum_protected_count") && event.key?("unchanged_count")
|
|
353
|
+
end
|
|
354
|
+
return unless summary
|
|
355
|
+
|
|
356
|
+
{
|
|
357
|
+
checksum_hits: summary.fetch("checksum_hit_count").to_i,
|
|
358
|
+
checksum_protected: summary.fetch("checksum_protected_count").to_i,
|
|
359
|
+
unchanged: summary.fetch("unchanged_count").to_i
|
|
360
|
+
}
|
|
361
|
+
end
|
|
362
|
+
|
|
342
363
|
def template_events(output)
|
|
343
364
|
output.to_s.each_line.filter_map do |line|
|
|
344
365
|
payload = JSON.parse(line)
|
|
@@ -5,7 +5,7 @@ module Kettle
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = "1.2.
|
|
8
|
+
VERSION = "1.2.18"
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
|
@@ -513,8 +513,8 @@ module Kettle
|
|
|
513
513
|
dirty_paths = GitStatus.dirty_paths(member.root)
|
|
514
514
|
next if dirty_paths.empty?
|
|
515
515
|
|
|
516
|
-
if recoverable_template_lockfile_dirt?(
|
|
517
|
-
|
|
516
|
+
if recoverable_template_lockfile_dirt?(dirty_paths)
|
|
517
|
+
recover_dirty_template_lockfile_before_branch_checkout(member: member, runner: command_runner, memo: results)
|
|
518
518
|
next if results.last&.ok? && GitStatus.dirty_paths(member.root).empty?
|
|
519
519
|
end
|
|
520
520
|
|
|
@@ -522,11 +522,25 @@ module Kettle
|
|
|
522
522
|
end
|
|
523
523
|
end
|
|
524
524
|
|
|
525
|
-
def recoverable_template_lockfile_dirt?(
|
|
525
|
+
def recoverable_template_lockfile_dirt?(dirty_paths)
|
|
526
526
|
return false unless command == "template" && config.normalize_lockfiles?
|
|
527
527
|
return false unless dirty_paths.length == 1 && dirty_paths.first.end_with?(" Gemfile.lock")
|
|
528
528
|
|
|
529
|
-
|
|
529
|
+
true
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def recover_dirty_template_lockfile_before_branch_checkout(member:, runner:, memo:)
|
|
533
|
+
if release_lockfile_has_local_path_remote?(member)
|
|
534
|
+
recover_template_lockfiles(member: member, runner: runner, memo: memo, phase: "template_lockfile_recovery")
|
|
535
|
+
else
|
|
536
|
+
commit_normalized_lockfiles(
|
|
537
|
+
branch_members: [member],
|
|
538
|
+
runner: runner,
|
|
539
|
+
memo: memo,
|
|
540
|
+
reason: "template",
|
|
541
|
+
force: true
|
|
542
|
+
)
|
|
543
|
+
end
|
|
530
544
|
end
|
|
531
545
|
|
|
532
546
|
def branch_checkout_preflight_required?
|
|
@@ -1897,9 +1911,15 @@ module Kettle
|
|
|
1897
1911
|
index = argv.index("kettle-jem")
|
|
1898
1912
|
return command_text unless index
|
|
1899
1913
|
|
|
1900
|
-
argv[0...index]
|
|
1914
|
+
prefix = argv[0...index]
|
|
1915
|
+
prefix = prefix[0...-2] if prefix.last(2) == %w[bundle exec]
|
|
1916
|
+
prefix + [RbConfig.ruby, executable] + argv[(index + 1)..]
|
|
1901
1917
|
else
|
|
1902
|
-
|
|
1918
|
+
replacement = "#{Shellwords.escape(RbConfig.ruby)} #{Shellwords.escape(executable)}"
|
|
1919
|
+
text = command_text.to_s
|
|
1920
|
+
return text.sub(/\bbundle\s+exec\s+kettle-jem\b/, replacement) if text.match?(/\bbundle\s+exec\s+kettle-jem\b/)
|
|
1921
|
+
|
|
1922
|
+
text.sub(/\bkettle-jem\b/, replacement)
|
|
1903
1923
|
end
|
|
1904
1924
|
end
|
|
1905
1925
|
|
|
@@ -2040,8 +2060,7 @@ module Kettle
|
|
|
2040
2060
|
end
|
|
2041
2061
|
|
|
2042
2062
|
def template_member_finish_status(result)
|
|
2043
|
-
|
|
2044
|
-
"#{changed_files} file#{"s" unless changed_files == 1} changed"
|
|
2063
|
+
template_summary_label(template_changed_file_count(result), template_file_outcomes(result))
|
|
2045
2064
|
end
|
|
2046
2065
|
|
|
2047
2066
|
def release_member_finish_status(result)
|
|
@@ -2053,8 +2072,9 @@ module Kettle
|
|
|
2053
2072
|
|
|
2054
2073
|
template_results = results.select { |result| result.phase == "template" }
|
|
2055
2074
|
changed_files = template_results.sum { |result| template_changed_file_count(result) }
|
|
2075
|
+
outcome_counts = template_results.map { |result| template_file_outcomes(result) }
|
|
2056
2076
|
progress.stop
|
|
2057
|
-
progress.summary(
|
|
2077
|
+
progress.summary(template_progress_summary_label(template_results, changed_files, outcome_counts))
|
|
2058
2078
|
end
|
|
2059
2079
|
|
|
2060
2080
|
def emit_release_progress_summary(results, progress:)
|
|
@@ -2119,7 +2139,7 @@ module Kettle
|
|
|
2119
2139
|
when "diagnostic"
|
|
2120
2140
|
emit_template_event_line(member, "!", diagnostic_event_label(event))
|
|
2121
2141
|
when "summary"
|
|
2122
|
-
emit_template_event_line(member, "done",
|
|
2142
|
+
emit_template_event_line(member, "done", template_event_summary_label(event))
|
|
2123
2143
|
end
|
|
2124
2144
|
end
|
|
2125
2145
|
|
|
@@ -2134,7 +2154,7 @@ module Kettle
|
|
|
2134
2154
|
when "diagnostic"
|
|
2135
2155
|
diagnostic_event_label(event)
|
|
2136
2156
|
when "summary"
|
|
2137
|
-
|
|
2157
|
+
template_event_summary_label(event)
|
|
2138
2158
|
end
|
|
2139
2159
|
mark = template_event_status_mark(event)
|
|
2140
2160
|
progress&.update(member, status: status, mark: mark) if status && !status.empty?
|
|
@@ -2394,6 +2414,61 @@ module Kettle
|
|
|
2394
2414
|
summary&.fetch("changed_count")&.to_i
|
|
2395
2415
|
end
|
|
2396
2416
|
|
|
2417
|
+
def template_file_outcomes(result)
|
|
2418
|
+
summaries = result.stdout.to_s.lines.filter_map do |line|
|
|
2419
|
+
event = parse_template_event(line)
|
|
2420
|
+
event if event && event["type"] == "summary"
|
|
2421
|
+
end
|
|
2422
|
+
summary = summaries.reverse.find { |event| template_file_outcome_event?(event) }
|
|
2423
|
+
return unless summary
|
|
2424
|
+
|
|
2425
|
+
{
|
|
2426
|
+
checksum_hits: summary.fetch("checksum_hit_count").to_i,
|
|
2427
|
+
checksum_protected: summary.fetch("checksum_protected_count").to_i,
|
|
2428
|
+
unchanged: summary.fetch("unchanged_count").to_i
|
|
2429
|
+
}
|
|
2430
|
+
end
|
|
2431
|
+
|
|
2432
|
+
def template_file_outcome_event?(event)
|
|
2433
|
+
event.key?("checksum_hit_count") && event.key?("checksum_protected_count") && event.key?("unchanged_count")
|
|
2434
|
+
end
|
|
2435
|
+
|
|
2436
|
+
def template_event_summary_label(event)
|
|
2437
|
+
template_summary_label(
|
|
2438
|
+
event["changed_count"].to_i,
|
|
2439
|
+
template_file_outcome_event?(event) ? {
|
|
2440
|
+
checksum_hits: event.fetch("checksum_hit_count").to_i,
|
|
2441
|
+
checksum_protected: event.fetch("checksum_protected_count").to_i,
|
|
2442
|
+
unchanged: event.fetch("unchanged_count").to_i
|
|
2443
|
+
} : nil
|
|
2444
|
+
)
|
|
2445
|
+
end
|
|
2446
|
+
|
|
2447
|
+
def template_progress_summary_label(template_results, changed_files, outcome_counts)
|
|
2448
|
+
label = "template summary: #{template_results.count(&:ok?)}/#{template_results.length} members ok"
|
|
2449
|
+
return "#{label}, #{template_summary_label(changed_files)}" unless outcome_counts.all?
|
|
2450
|
+
|
|
2451
|
+
totals = outcome_counts.each_with_object({checksum_hits: 0, checksum_protected: 0, unchanged: 0}) do |outcomes, memo|
|
|
2452
|
+
memo[:checksum_hits] += outcomes.fetch(:checksum_hits)
|
|
2453
|
+
memo[:checksum_protected] += outcomes.fetch(:checksum_protected)
|
|
2454
|
+
memo[:unchanged] += outcomes.fetch(:unchanged)
|
|
2455
|
+
memo
|
|
2456
|
+
end
|
|
2457
|
+
"#{label}, #{template_summary_label(changed_files, totals)}"
|
|
2458
|
+
end
|
|
2459
|
+
|
|
2460
|
+
def template_summary_label(changed_files, outcomes = nil)
|
|
2461
|
+
changed_label = "#{changed_files} file#{"s" unless changed_files == 1} changed"
|
|
2462
|
+
return changed_label unless outcomes
|
|
2463
|
+
|
|
2464
|
+
protected_label = if outcomes.fetch(:checksum_protected).positive?
|
|
2465
|
+
", #{outcomes.fetch(:checksum_protected)} checksum-protected changes"
|
|
2466
|
+
else
|
|
2467
|
+
""
|
|
2468
|
+
end
|
|
2469
|
+
"#{outcomes.fetch(:checksum_hits)} checksum hits#{protected_label}, #{outcomes.fetch(:unchanged)} unchanged, #{changed_label}"
|
|
2470
|
+
end
|
|
2471
|
+
|
|
2397
2472
|
def normalize_lockfiles(member:, runner:, memo:, phase:)
|
|
2398
2473
|
return unless config.normalize_lockfiles?
|
|
2399
2474
|
|
|
@@ -2574,6 +2649,22 @@ module Kettle
|
|
|
2574
2649
|
command: template_prepare_command(member),
|
|
2575
2650
|
env: template_prepare_env
|
|
2576
2651
|
)
|
|
2652
|
+
if recoverable_bundle_failure?(result)
|
|
2653
|
+
recover_template_lockfiles(
|
|
2654
|
+
member: member,
|
|
2655
|
+
runner: runner,
|
|
2656
|
+
memo: memo,
|
|
2657
|
+
phase: "prepare_template_dependencies_recovery"
|
|
2658
|
+
)
|
|
2659
|
+
return false unless memo.last&.ok?
|
|
2660
|
+
|
|
2661
|
+
result = runner.call(
|
|
2662
|
+
member: member,
|
|
2663
|
+
phase: "prepare_template_dependencies",
|
|
2664
|
+
command: template_prepare_command(member),
|
|
2665
|
+
env: template_prepare_env
|
|
2666
|
+
)
|
|
2667
|
+
end
|
|
2577
2668
|
memo << result
|
|
2578
2669
|
result.ok?
|
|
2579
2670
|
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kettle-family
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -230,7 +230,7 @@ dependencies:
|
|
|
230
230
|
version: '3.2'
|
|
231
231
|
- - ">="
|
|
232
232
|
- !ruby/object:Gem::Version
|
|
233
|
-
version: 3.2.
|
|
233
|
+
version: 3.2.4
|
|
234
234
|
type: :development
|
|
235
235
|
prerelease: false
|
|
236
236
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -240,7 +240,7 @@ dependencies:
|
|
|
240
240
|
version: '3.2'
|
|
241
241
|
- - ">="
|
|
242
242
|
- !ruby/object:Gem::Version
|
|
243
|
-
version: 3.2.
|
|
243
|
+
version: 3.2.4
|
|
244
244
|
- !ruby/object:Gem::Dependency
|
|
245
245
|
name: ruby-progressbar
|
|
246
246
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -264,7 +264,7 @@ dependencies:
|
|
|
264
264
|
version: '1.0'
|
|
265
265
|
- - ">="
|
|
266
266
|
- !ruby/object:Gem::Version
|
|
267
|
-
version: 1.0.
|
|
267
|
+
version: 1.0.8
|
|
268
268
|
type: :development
|
|
269
269
|
prerelease: false
|
|
270
270
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -274,7 +274,7 @@ dependencies:
|
|
|
274
274
|
version: '1.0'
|
|
275
275
|
- - ">="
|
|
276
276
|
- !ruby/object:Gem::Version
|
|
277
|
-
version: 1.0.
|
|
277
|
+
version: 1.0.8
|
|
278
278
|
- !ruby/object:Gem::Dependency
|
|
279
279
|
name: gitmoji-regex
|
|
280
280
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -284,7 +284,7 @@ dependencies:
|
|
|
284
284
|
version: '2.0'
|
|
285
285
|
- - ">="
|
|
286
286
|
- !ruby/object:Gem::Version
|
|
287
|
-
version: 2.0.
|
|
287
|
+
version: 2.0.11
|
|
288
288
|
type: :development
|
|
289
289
|
prerelease: false
|
|
290
290
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -294,7 +294,7 @@ dependencies:
|
|
|
294
294
|
version: '2.0'
|
|
295
295
|
- - ">="
|
|
296
296
|
- !ruby/object:Gem::Version
|
|
297
|
-
version: 2.0.
|
|
297
|
+
version: 2.0.11
|
|
298
298
|
description: "\U0001F469\U0001F469\U0001F467\U0001F467 Kettle::Family provides
|
|
299
299
|
scripts and conventions for coordinating related Ruby gems as one family."
|
|
300
300
|
email:
|
|
@@ -342,10 +342,10 @@ licenses:
|
|
|
342
342
|
- AGPL-3.0-only
|
|
343
343
|
metadata:
|
|
344
344
|
homepage_uri: https://kettle-family.galtzo.com
|
|
345
|
-
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.
|
|
346
|
-
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.
|
|
345
|
+
source_code_uri: https://github.com/kettle-dev/kettle-family/tree/v1.2.18
|
|
346
|
+
changelog_uri: https://github.com/kettle-dev/kettle-family/blob/v1.2.18/CHANGELOG.md
|
|
347
347
|
bug_tracker_uri: https://github.com/kettle-dev/kettle-family/issues
|
|
348
|
-
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.
|
|
348
|
+
documentation_uri: https://www.rubydoc.info/gems/kettle-family/1.2.18
|
|
349
349
|
funding_uri: https://github.com/sponsors/pboling
|
|
350
350
|
wiki_uri: https://github.com/kettle-dev/kettle-family/wiki
|
|
351
351
|
news_uri: https://www.railsbling.com/tags/kettle-family
|
metadata.gz.sig
CHANGED
|
Binary file
|