gem_hadar 1.24.0 → 1.26.0

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.
data/lib/gem_hadar.rb CHANGED
@@ -25,12 +25,16 @@ require_maybe 'rspec/core/rake_task'
25
25
  class GemHadar
26
26
  end
27
27
  require 'gem_hadar/version'
28
+ require 'gem_hadar/utils'
28
29
  require 'gem_hadar/setup'
29
30
  require 'gem_hadar/template_compiler'
30
31
  require 'gem_hadar/github'
32
+ require 'gem_hadar/prompt_template'
31
33
 
32
34
  class GemHadar
33
35
  include Term::ANSIColor
36
+ include GemHadar::Utils
37
+ include GemHadar::PromptTemplate
34
38
 
35
39
  if defined?(::RbConfig)
36
40
  include ::RbConfig
@@ -47,6 +51,12 @@ class GemHadar
47
51
  block and instance_eval(&block)
48
52
  end
49
53
 
54
+ # The has_to_be_set method raises an error if a required gem configuration
55
+ # attribute is not set.
56
+ #
57
+ # @param name [ String ] the name of the required attribute
58
+ #
59
+ # @raise [ ArgumentError ] if the specified attribute has not been set
50
60
  def has_to_be_set(name)
51
61
  fail "#{self.class}: #{name} has to be set for gem"
52
62
  end
@@ -130,7 +140,7 @@ class GemHadar
130
140
  end
131
141
 
132
142
  dsl_accessor :yard_dir do
133
- 'yard'
143
+ 'doc'
134
144
  end
135
145
 
136
146
  dsl_accessor :extensions do FileList['ext/**/extconf.rb'] end
@@ -178,6 +188,18 @@ class GemHadar
178
188
  dsl_accessor :gemset do @outer_scope.name end
179
189
  end
180
190
 
191
+ # The rvm method configures RVM (Ruby Version Manager) settings for the gem
192
+ # project.
193
+ #
194
+ # This method initializes and returns an RvmConfig object that holds RVM-specific
195
+ # configuration such as the Ruby version to use and the gemset name.
196
+ # If a block is provided, it configures the RvmConfig object with the given
197
+ # settings. If no block is provided and no existing RvmConfig object exists,
198
+ # it creates a new one with default settings.
199
+ #
200
+ # @param block [ Proc ] optional block to configure RVM settings
201
+ #
202
+ # @return [ GemHadar::RvmConfig ] the RVM configuration object
181
203
  def rvm(&block)
182
204
  if block
183
205
  @rvm = RvmConfig.new(&block)
@@ -189,6 +211,12 @@ class GemHadar
189
211
 
190
212
  dsl_accessor :default_task_dependencies, [ :gemspec, :test ]
191
213
 
214
+ # The default_task method defines the default Rake task for the gem project.
215
+ #
216
+ # This method sets up a Rake task named :default that depends on the tasks
217
+ # specified in the default_task_dependencies accessor. It provides a convenient
218
+ # way to run the most common or essential tasks for the project with a single
219
+ # command.
192
220
  def default_task
193
221
  desc 'Default task'
194
222
  task :default => default_task_dependencies
@@ -196,11 +224,26 @@ class GemHadar
196
224
 
197
225
  dsl_accessor :build_task_dependencies, [ :clobber, :gemspec, :package, :'version:tag' ]
198
226
 
227
+ # The build_task method defines a Rake task that orchestrates the complete
228
+ # build process for packaging the gem.
229
+ #
230
+ # This method sets up a :build task that depends on the tasks specified in
231
+ # the build_task_dependencies accessor. It provides a convenient way to
232
+ # execute all necessary steps for building packages for a release with a
233
+ # single command.
199
234
  def build_task
200
235
  desc 'Build task (builds all packages for a release)'
201
236
  task :build => build_task_dependencies
202
237
  end
203
238
 
239
+ # The install_library method sets up a Rake task for installing the library
240
+ # or executable into site_ruby directories.
241
+ #
242
+ # This method configures an :install task that depends on the
243
+ # :prepare_install task and executes the provided block. It stores the block
244
+ # in an instance variable to be called later when the task is executed.
245
+ #
246
+ # @param block [ Proc ] the block containing the installation logic
204
247
  def install_library(&block)
205
248
  @install_library_block = -> do
206
249
  desc 'Install executable/library into site_ruby directories'
@@ -208,6 +251,15 @@ class GemHadar
208
251
  end
209
252
  end
210
253
 
254
+ # The clean method manages the CLEAN file list for Rake tasks.
255
+ #
256
+ # When called without arguments, it returns the current CLEAN file list.
257
+ # When called with arguments, it adds the specified files to the CLEAN list.
258
+ #
259
+ # @param args [ Array<String> ] optional list of files to add to the CLEAN list
260
+ #
261
+ # @return [ FileList, nil ] the CLEAN file list when no arguments provided,
262
+ # nil otherwise
211
263
  def clean(*args)
212
264
  if args.empty?
213
265
  CLEAN
@@ -216,6 +268,15 @@ class GemHadar
216
268
  end
217
269
  end
218
270
 
271
+ # The clobber method manages the CLOBBER file list for Rake tasks.
272
+ #
273
+ # When called without arguments, it returns the current CLOBBER file list.
274
+ # When called with arguments, it adds the specified files to the CLOBBER list.
275
+ #
276
+ # @param args [ Array<String> ] optional list of files to add to the CLOBBER list
277
+ #
278
+ # @return [ FileList, nil ] the CLOBBER file list when no arguments provided,
279
+ # nil otherwise
219
280
  def clobber(*args)
220
281
  if args.empty?
221
282
  CLOBBER
@@ -224,6 +285,15 @@ class GemHadar
224
285
  end
225
286
  end
226
287
 
288
+ # The ignore method manages the list of files to be ignored by the gem.
289
+ #
290
+ # When called without arguments, it returns the current set of ignored files.
291
+ # When called with arguments, it adds the specified files to the ignore list.
292
+ #
293
+ # @param args [ Array<String> ] optional list of file patterns to add to the ignore list
294
+ #
295
+ # @return [ Set<String>, nil ] the set of ignored files when no arguments provided,
296
+ # nil otherwise
227
297
  def ignore(*args)
228
298
  if args.empty?
229
299
  ignore_files
@@ -232,6 +302,17 @@ class GemHadar
232
302
  end
233
303
  end
234
304
 
305
+ # The package_ignore method manages the list of files to be ignored during
306
+ # gem packaging.
307
+ #
308
+ # When called without arguments, it returns the current set of package ignore
309
+ # files. When called with arguments, it adds the specified file patterns to
310
+ # the package ignore list.
311
+ #
312
+ # @param args [ Array<String> ] optional list of file patterns to add to the package ignore list
313
+ #
314
+ # @return [ Set<String>, nil ] the set of package ignore files when no arguments provided,
315
+ # nil otherwise
235
316
  def package_ignore(*args)
236
317
  if args.empty?
237
318
  package_ignore_files
@@ -240,14 +321,28 @@ class GemHadar
240
321
  end
241
322
  end
242
323
 
324
+ # The dependency method adds a new runtime dependency to the gem.
325
+ #
326
+ # @param args [ Array ] the arguments defining the dependency
243
327
  def dependency(*args)
244
328
  @dependencies << args
245
329
  end
246
330
 
331
+ # The development_dependency method adds a new development-time dependency to
332
+ # the gem.
333
+ #
334
+ # @param args [ Array ] the arguments defining the development dependency
247
335
  def development_dependency(*args)
248
336
  @development_dependencies << args
249
337
  end
250
338
 
339
+ # The gems_install_task method defines a Rake task for installing all gem
340
+ # dependencies specified in the Gemfile.
341
+ #
342
+ # This method sets up a :gems:install task that executes a block to install
343
+ # gems. If no block is provided, it defaults to running 'bundle install'.
344
+ #
345
+ # @param block [ Proc ] optional block containing the installation command
251
346
  def gems_install_task(&block)
252
347
  block ||= proc { sh 'bundle install' }
253
348
  desc 'Install all gems from the Gemfile'
@@ -256,6 +351,14 @@ class GemHadar
256
351
  end
257
352
  end
258
353
 
354
+ # The version_task method defines a Rake task that generates a version file
355
+ # for the gem.
356
+ #
357
+ # This method creates a task named :version that writes version information
358
+ # to a Ruby file in the lib directory. The generated file contains constants
359
+ # for the version and its components, as well as an optional epilogue
360
+ # section. The task ensures the target directory exists and uses secure file
361
+ # writing to prevent permission issues.
259
362
  def version_task
260
363
  desc m = "Writing version information for #{name}-#{version}"
261
364
  task :version do
@@ -277,6 +380,13 @@ class GemHadar
277
380
  end
278
381
  end
279
382
 
383
+ # The version_show_task method defines a Rake task that displays the current
384
+ # version of the gem.
385
+ #
386
+ # This method creates a :version:show task under the Rake namespace that
387
+ # reads the version from the generated version file in the lib directory and
388
+ # compares it with the version specified in the GemHadar configuration. It
389
+ # then outputs a message indicating whether the versions match or not.
280
390
  def version_show_task
281
391
  namespace :version do
282
392
  desc "Displaying the current version"
@@ -295,6 +405,21 @@ class GemHadar
295
405
  end
296
406
  end
297
407
 
408
+ # The version_log_diff method generates a git log output containing patch
409
+ # differences between two specified versions.
410
+ #
411
+ # This method retrieves the commit history between a starting version and an
412
+ # ending version, including detailed changes (patch format) for each commit.
413
+ # It supports comparing against the current HEAD or specific version tags,
414
+ # and automatically determines the appropriate previous version when only a
415
+ # target version is provided.
416
+ #
417
+ # @param to_version [ String ] the ending version tag or 'HEAD' to compare up to the latest commit
418
+ # @param from_version [ String, nil ] the starting version tag; if nil, it defaults based on to_version
419
+ #
420
+ # @raise [ RuntimeError ] if the specified version tags are not found in the repository
421
+ #
422
+ # @return [ String ] the git log output in patch format showing changes between the two versions
298
423
  def version_log_diff(to_version: 'HEAD', from_version: nil)
299
424
  if to_version == 'HEAD'
300
425
  if from_version.blank?
@@ -327,6 +452,15 @@ class GemHadar
327
452
  end
328
453
  end
329
454
 
455
+ # The version_diff_task method defines Rake tasks for listing and displaying
456
+ # git version differences.
457
+ #
458
+ # This method sets up two subtasks under the :version namespace:
459
+ #
460
+ # - A :list task that fetches all git tags, ensures the operation succeeds,
461
+ # and outputs the sorted list of versions.
462
+ # - A :diff task that calculates the version range, displays a colored diff
463
+ # between the versions, and shows the changes.
330
464
  def version_diff_task
331
465
  namespace :version do
332
466
  desc "List all versions in order"
@@ -338,19 +472,25 @@ class GemHadar
338
472
 
339
473
  desc "Displaying the diff from env var VERSION to the next version or HEAD"
340
474
  task :diff do
341
- version_tags = versions.map { version_tag(_1) } + %w[ HEAD ]
342
- found_version_tag = version_tags.index(version_tag(version))
343
- found_version_tag.nil? and fail "cannot find version tag #{version_tag(version)}"
344
- start_version, end_version = version_tags[found_version_tag, 2]
475
+ start_version, end_version = determine_version_range
345
476
  puts color(172) { "Showing diff from version %s to %s:" % [ start_version, end_version ] }
346
477
  puts `git diff --color=always #{start_version}..#{end_version}`
347
478
  end
348
479
  end
349
480
  end
350
481
 
482
+ # The gem_hadar_update_task method defines a Rake task that updates the
483
+ # gem_hadar dependency version in the gemspec file.
484
+ #
485
+ # This method creates a :gem_hadar:update task under the Rake namespace that
486
+ # prompts the user to specify a new gem_hadar version.
487
+ # It then reads the existing gemspec file, modifies the version constraint
488
+ # for the gem_hadar dependency, and writes the updated content back to the
489
+ # file. If the specified version is already present in the gemspec, it skips
490
+ # the update and notifies the user.
351
491
  def gem_hadar_update_task
352
492
  namespace :gem_hadar do
353
- desc 'Update gem_hadar a different version'
493
+ desc 'Update gem_hadar to a different version'
354
494
  task :update do
355
495
  answer = ask?("Which gem_hadar version? ", /^((?:\d+.){2}(?:\d+))$/)
356
496
  unless answer
@@ -376,6 +516,14 @@ class GemHadar
376
516
  end
377
517
  end
378
518
 
519
+ # The gemspec_task method defines a Rake task that generates and writes a
520
+ # gemspec file for the project.
521
+ #
522
+ # This method creates a :gemspec task that depends on the :version task,
523
+ # ensuring the version is set before generating the gemspec. It constructs
524
+ # the filename based on the project name, displays a warning message
525
+ # indicating the file being written, and uses secure_write to create the
526
+ # gemspec file with content generated by the gemspec method.
379
527
  def gemspec_task
380
528
  desc 'Create a gemspec file'
381
529
  task :gemspec => :version do
@@ -385,6 +533,12 @@ class GemHadar
385
533
  end
386
534
  end
387
535
 
536
+ # The package_task method sets up a Rake task for packaging the gem.
537
+ #
538
+ # This method configures a task that creates a package directory, initializes
539
+ # a Gem::PackageTask with the current gem specification, and specifies that
540
+ # tar files should be created. It also includes the files to be packaged by
541
+ # adding gem_files to the package_files attribute of the Gem::PackageTask.
388
542
  def package_task
389
543
  clean 'pkg'
390
544
  Gem::PackageTask.new(gemspec) do |pkg|
@@ -393,24 +547,20 @@ class GemHadar
393
547
  end
394
548
  end
395
549
 
550
+ # The install_library_task method executes the installed library task block
551
+ # if it has been defined.
396
552
  def install_library_task
397
553
  @install_library_block.full?(:call)
398
554
  end
399
555
 
400
- def doc_task
401
- clean 'doc'
402
- desc "Creating documentation"
403
- task :doc do
404
- sh 'yard doc'
405
- cmd = 'yardoc'
406
- if readme
407
- cmd << " --readme '#{readme}'"
408
- end
409
- cmd << ' - ' << doc_files * ' '
410
- sh cmd
411
- end
412
- end
413
-
556
+ # The test_task method sets up a Rake task for executing the project's test
557
+ # suite.
558
+ #
559
+ # This method configures a Rake task named :test that runs the test suite
560
+ # using Rake::TestTask. It includes the test directory and required paths in
561
+ # the load path, specifies the test files to run, and enables verbose output.
562
+ # The task also conditionally depends on the :compile task if project
563
+ # extensions are present.
414
564
  def test_task
415
565
  tt = Rake::TestTask.new(:run_tests) do |t|
416
566
  t.libs << test_dir
@@ -422,6 +572,12 @@ class GemHadar
422
572
  task :test => [ (:compile if extensions.full?), tt.name ].compact
423
573
  end
424
574
 
575
+ # The spec_task method sets up a Rake task for executing RSpec tests.
576
+ #
577
+ # This method configures a :spec task that runs the project's RSpec test
578
+ # suite. It initializes an RSpec::Core::RakeTask with appropriate Ruby
579
+ # options, test pattern, and verbose output. The task also conditionally
580
+ # depends on the :compile task if project extensions are present.
425
581
  def spec_task
426
582
  defined? ::RSpec::Core::RakeTask or return
427
583
  st = RSpec::Core::RakeTask.new(:run_specs) do |t|
@@ -433,6 +589,15 @@ class GemHadar
433
589
  task :spec => [ (:compile if extensions.full?), st.name ].compact
434
590
  end
435
591
 
592
+ # The rcov_task method sets up a Rake task for executing code coverage tests
593
+ # using RCov.
594
+ #
595
+ # This method configures a :rcov task that runs the project's test suite with
596
+ # RCov to generate code coverage reports. It includes the test directory and
597
+ # required paths in the load path, specifies the test files to run, and
598
+ # enables verbose output. The task also conditionally depends on the :compile
599
+ # task if project extensions are present. If RCov is not available, it
600
+ # displays a warning message suggesting to install RCov.
436
601
  def rcov_task
437
602
  if defined?(::Rcov)
438
603
  rt = ::Rcov::RcovTask.new(:run_rcov) do |t|
@@ -454,33 +619,76 @@ class GemHadar
454
619
  end
455
620
  end
456
621
 
622
+ # The version_bump_task method defines Rake tasks for incrementing the gem's
623
+ # version number.
624
+ #
625
+ # This method sets up a hierarchical task structure under the :version
626
+ # namespace:
627
+ #
628
+ # - It creates subtasks in the :version:bump namespace for explicitly bumping
629
+ # major, minor, or build versions.
630
+ # - It also defines a :version:bump task that automatically suggests the
631
+ # appropriate version bump type by analyzing recent changes using AI. The
632
+ # suggestion is based on the git log diff between the previous version and
633
+ # the current HEAD, and it prompts the user for confirmation before applying
634
+ # the bump.
635
+ #
636
+ # The tasks utilize the version_log_diff method to gather change information,
637
+ # the ollama_generate method to get AI-powered suggestions, and the
638
+ # version_bump_to method to perform the actual version update.
457
639
  def version_bump_task
458
640
  namespace :version do
459
641
  namespace :bump do
460
642
  desc 'Bump major version'
461
643
  task :major do
462
- version = File.read('VERSION').chomp.version
463
- version.bump(:major)
464
- secure_write('VERSION') { |v| v.puts version }
644
+ version_bump_to(:major)
465
645
  end
466
646
 
467
647
  desc 'Bump minor version'
468
648
  task :minor do
469
- version = File.read('VERSION').chomp.version
470
- version.bump(:minor)
471
- secure_write('VERSION') { |v| v.puts version }
649
+ version_bump_to(:minor)
472
650
  end
473
651
 
474
652
  desc 'Bump build version'
475
653
  task :build do
476
- version = File.read('VERSION').chomp.version
477
- version.bump(:build)
478
- secure_write('VERSION') { |v| v.puts version }
654
+ version_bump_to(:build)
655
+ end
656
+ end
657
+
658
+ desc 'Bump version with AI suggestion'
659
+ task :bump do
660
+ log_diff = version_log_diff(from_version: nil, to_version: 'HEAD')
661
+ system = xdg_config('version_bump_system_prompt.txt', default_version_bump_system_prompt)
662
+ prompt = xdg_config('version_bump_prompt.txt', default_version_bump_prompt) % { version:, log_diff: }
663
+ response = ollama_generate(system:, prompt:)
664
+ puts response
665
+ default = nil
666
+ if response =~ /(major|minor|build)\s*$/
667
+ default = $1
668
+ end
669
+ response = ask?(
670
+ 'Bump a major, minor, or build version%{default}? ',
671
+ /\A(major|minor|build)\z/,
672
+ default:
673
+ )
674
+ if version_type = response&.[](1)
675
+ version_bump_to(version_type)
676
+ else
677
+ exit 1
479
678
  end
480
679
  end
481
680
  end
482
681
  end
483
682
 
683
+ # The version_tag_task method defines a Rake task that creates a Git tag for
684
+ # the current version.
685
+ #
686
+ # This method sets up a :version:tag task under the Rake namespace that
687
+ # creates an annotated Git tag for the project's current version. It checks
688
+ # if a tag with the same name already exists and handles the case where the
689
+ # tag exists but is different from the current commit. If the tag already
690
+ # exists and is different, it prompts the user to confirm whether to
691
+ # overwrite it forcefully.
484
692
  def version_tag_task
485
693
  namespace :version do
486
694
  desc "Tag this commit as version #{version}"
@@ -505,10 +713,24 @@ class GemHadar
505
713
  end
506
714
  end
507
715
 
716
+ # The git_remote method retrieves the primary Git remote name configured for
717
+ # the project.
718
+ #
719
+ # It first checks the GIT_REMOTE environment variable for a custom remote
720
+ # specification. If not set, it defaults to 'origin'. When multiple remotes
721
+ # are specified in the environment variable, only the first one is returned.
508
722
  def git_remote
509
723
  ENV.fetch('GIT_REMOTE', 'origin').split(/\s+/).first
510
724
  end
511
725
 
726
+ # The master_prepare_task method defines a Rake task that sets up a remote
727
+ # Git repository for the project.
728
+ #
729
+ # This method creates a :master:prepare task under the Rake namespace that
730
+ # guides the user through creating a new bare Git repository on a remote
731
+ # server via SSH. It prompts for the remote name, directory path, and SSH
732
+ # account details to configure the repository and establish a connection back
733
+ # to the local project.
512
734
  def master_prepare_task
513
735
  namespace :master do
514
736
  desc "Prepare a remote git repository for this project"
@@ -526,6 +748,20 @@ class GemHadar
526
748
  end
527
749
  end
528
750
 
751
+ # The version_push_task method defines Rake tasks for pushing version tags to
752
+ # Git remotes.
753
+ #
754
+ # This method sets up a hierarchical task structure under the :version
755
+ # namespace:
756
+ #
757
+ # - It creates subtasks in the :version:push namespace for each configured
758
+ # Git remote, allowing individual pushes to specific remotes.
759
+ # - It also defines a top-level :version:push task that depends on all the
760
+ # individual remote push tasks, enabling a single command to push the version
761
+ # tag to all remotes.
762
+ #
763
+ # The tasks utilize the git_remotes method to determine which remotes are
764
+ # configured and generate appropriate push commands for each one.
529
765
  def version_push_task
530
766
  namespace :version do
531
767
  git_remotes.each do |gr|
@@ -542,6 +778,19 @@ class GemHadar
542
778
  end
543
779
  end
544
780
 
781
+ # The master_push_task method defines Rake tasks for pushing the master
782
+ # branch to configured Git remotes.
783
+ #
784
+ # This method sets up a hierarchical task structure under the :master namespace:
785
+ #
786
+ # - It creates subtasks in the :master:push namespace for each configured Git
787
+ # remote, allowing individual pushes to specific remotes.
788
+ # - It also defines a top-level :master:push task that depends on all the individual
789
+ # remote push tasks, enabling a single command to push the master branch to
790
+ # all remotes.
791
+ #
792
+ # The tasks utilize the git_remotes method to determine which remotes are
793
+ # configured and generate appropriate push commands for each one.
545
794
  def master_push_task
546
795
  namespace :master do
547
796
  git_remotes.each do |gr|
@@ -558,6 +807,17 @@ class GemHadar
558
807
  end
559
808
  end
560
809
 
810
+ # The gem_push_task method defines a Rake task for pushing the generated gem
811
+ # file to RubyGems.
812
+ #
813
+ # This method sets up a :gem:push task under the Rake namespace that handles
814
+ # the process of uploading the gem package to RubyGems. It checks if the
815
+ # project is in developing mode and skips the push operation if so.
816
+ # Otherwise, it verifies the existence of the gem file, prompts the user for
817
+ # confirmation before pushing, and uses the gem push command with an optional
818
+ # API key from the environment. If the gem file does not exist or the user
819
+ # declines to push, appropriate messages are displayed and the task exits
820
+ # accordingly.
561
821
  def gem_push_task
562
822
  namespace :gem do
563
823
  path = "pkg/#{name_version}.gem"
@@ -586,6 +846,15 @@ class GemHadar
586
846
  end
587
847
  end
588
848
 
849
+ # The git_remotes_task method defines a Rake task that displays all Git
850
+ # remote repositories configured for the project.
851
+ #
852
+ # This method sets up a :git_remotes task under the Rake namespace that
853
+ # retrieves and prints the list of Git remotes along with their URLs. It uses
854
+ # the git_remotes method to obtain the remote names and then fetches each
855
+ # remote's URL using the `git remote get-url` command. The output is
856
+ # formatted to show each remote name followed by its corresponding URL on
857
+ # separate lines.
589
858
  def git_remotes_task
590
859
  task :git_remotes do
591
860
  puts git_remotes.map { |r|
@@ -595,65 +864,29 @@ class GemHadar
595
864
  end
596
865
  end
597
866
 
598
- def create_body
599
- base_url = ENV['OLLAMA_URL']
600
- if base_url.blank? && host = ENV['OLLAMA_HOST'].full?
601
- base_url = 'http://%s' % host
602
- end
603
- base_url.present? or return
867
+ # The create_git_release_body method generates a changelog for a GitHub
868
+ # release by analyzing the git diff between the previous version and the
869
+ # current HEAD.
870
+ #
871
+ # It retrieves the log differences, fetches or uses default system and prompt
872
+ # templates, and utilizes an AI model to produce a formatted changelog entry.
873
+ #
874
+ # @return [ String ] the generated changelog content for the release body
875
+ def create_git_release_body
604
876
  log_diff = version_log_diff(to_version: version)
605
- model = ENV.fetch('OLLAMA_MODEL', 'llama3.1')
606
- ollama = Ollama::Client.new(base_url:, read_timeout: 600, connect_timeout: 60)
607
- system = <<~EOT
608
- You are a Ruby programmer generating changelog messages in markdown
609
- format for new releases, so users can see what has changed. Remember you
610
- are not a chatbot of any kind.
611
- EOT
612
- prompt = (<<~EOT) % { name:, version:, log_diff: }
613
- Output the content of a changelog for the new release of %{name} %{version}
614
-
615
- **Strictly** follow these guidelines:
616
-
617
- - Use bullet points in markdown format (`-`) to list significant changes.
618
- - Exclude trivial updates such as:
619
- * Version number increments
620
- * Dependency version bumps (unless they resolve critical issues)
621
- * Minor code style adjustments
622
- * Internal documentation tweaks
623
- - Include only verified and substantial changes that impact
624
- functionality, performance, or user experience.
625
- - If unsure about a change's significance, omit it from the output.
626
- - Avoid adding any comments or notes; keep the output purely factual.
627
-
628
- These are the log messages including patches for the new release:
629
-
630
- %{log_diff}
631
- EOT
632
- options = ENV['OLLAMA_OPTIONS'].full? { |o| JSON.parse(o) } || {}
633
- options |= { "temperature" => 0, "top_p" => 1, "min_p" => 0.1 }
634
- ollama.generate(model:, system:, prompt:, options:, stream: false, think: false).response
635
- end
636
-
637
- def edit_temp_file(content)
638
- editor = ENV.fetch('EDITOR', `which vi`.chomp)
639
- unless File.exist?(editor)
640
- warn "Can't find EDITOR. => Returning."
641
- return
642
- end
643
- temp_file = Tempfile.new('changelog')
644
- temp_file.write(content)
645
- temp_file.close
646
-
647
- unless system("#{editor} #{temp_file.path}")
648
- warn "#{editor} returned #{$?.exitstatus} => Returning."
649
- return
650
- end
651
-
652
- File.read(temp_file.path)
653
- ensure
654
- temp_file&.close&.unlink
877
+ system = xdg_config('release_system_prompt.txt', default_git_release_system_prompt)
878
+ prompt = xdg_config('release_prompt.txt', default_git_release_prompt) % { name:, version:, log_diff: }
879
+ ollama_generate(system:, prompt:)
655
880
  end
656
881
 
882
+ # The github_release_task method defines a Rake task that creates a GitHub
883
+ # release for the current version.
884
+ #
885
+ # This method sets up a :github:release task that prompts the user to confirm
886
+ # publishing a release message on GitHub. It retrieves the GitHub API token
887
+ # from the environment, derives the repository owner and name from the git
888
+ # remote URL, generates a changelog using AI, and creates the release via the
889
+ # GitHub API.
657
890
  def github_release_task
658
891
  namespace :github do
659
892
  unless github_api_token = ENV['GITHUB_API_TOKEN'].full?
@@ -661,7 +894,7 @@ class GemHadar
661
894
  task :release
662
895
  return
663
896
  end
664
- desc "Create a new GitHub release for the current version with a changelog"
897
+ desc "Create a new GitHub release for the current version with a AI-generated changelog"
665
898
  task :release do
666
899
  yes = ask?(
667
900
  "Do you want to publish a release message on github? (y/n%{default}) ",
@@ -675,7 +908,7 @@ class GemHadar
675
908
  rc = GitHub::ReleaseCreator.new(owner:, repo:, token: github_api_token)
676
909
  tag_name = version_tag(version)
677
910
  target_commitish = `git show -s --format=%H #{tag_name.inspect}^{commit}`.chomp
678
- body = edit_temp_file(create_body)
911
+ body = edit_temp_file(create_git_release_body)
679
912
  if body.present?
680
913
  begin
681
914
  response = rc.perform(tag_name:, target_commitish:, body:)
@@ -695,6 +928,14 @@ class GemHadar
695
928
 
696
929
  dsl_accessor :push_task_dependencies, %i[ modified build master:push version:push gem:push github:release ]
697
930
 
931
+ # The push_task method defines a Rake task that orchestrates the complete
932
+ # process of pushing changes and artifacts to remote repositories and package
933
+ # managers.
934
+ #
935
+ # This method sets up multiple subtasks including preparing the master
936
+ # branch, pushing version tags, pushing to gem repositories, and creating
937
+ # GitHub releases. It also includes a check for uncommitted changes before
938
+ # proceeding with the push operations.
698
939
  def push_task
699
940
  master_prepare_task
700
941
  version_push_task
@@ -709,10 +950,26 @@ class GemHadar
709
950
  exit 1
710
951
  end
711
952
  end
712
- desc "Push master and version #{version} all git remotes: #{git_remotes * ' '}"
953
+ desc "Push all changes for version #{version} into the internets."
713
954
  task :push => push_task_dependencies
714
955
  end
715
956
 
957
+ # The release_task method defines a Rake task that orchestrates the complete
958
+ # release process for the gem.
959
+ #
960
+ # This method sets up a :release task that depends on the :push task,
961
+ # ensuring all necessary steps for publishing the gem are executed in
962
+ # sequence. It provides a convenient way to perform a full release workflow
963
+ # with a single command.
964
+ def release_task
965
+ desc "Release the new version #{version} for the gem #{name}."
966
+ task :release => :push
967
+ end
968
+
969
+ # The compile_task method sets up a Rake task to compile project extensions.
970
+ #
971
+ # This method creates a :compile task that iterates through the configured
972
+ # extensions and compiles them using the system's make command.
716
973
  def compile_task
717
974
  for file in extensions
718
975
  dir = File.dirname(file)
@@ -730,6 +987,16 @@ class GemHadar
730
987
  end
731
988
  end
732
989
 
990
+ # The rvm_task method creates a .rvmrc file that configures RVM to use the
991
+ # specified Ruby version and gemset for the project.
992
+ #
993
+ # This task generates a .rvmrc file in the project root directory with commands to:
994
+ # - Use the Ruby version specified by the rvm.use accessor
995
+ # - Create the gemset specified by the rvm.gemset accessor
996
+ # - Switch to using that gemset
997
+ #
998
+ # The generated file is written using the secure_write method to ensure
999
+ # proper file permissions.
733
1000
  def rvm_task
734
1001
  desc 'Create .rvmrc file'
735
1002
  task :rvm do
@@ -743,15 +1010,55 @@ class GemHadar
743
1010
  end
744
1011
  end
745
1012
 
1013
+ def yard_doc_task
1014
+ YARD::Rake::YardocTask.new(:yard_doc) do |t|
1015
+ t.files = files.select { _1 =~ /\.rb\z/ }
1016
+
1017
+ output_dir = yard_dir
1018
+ t.options = [ "--output-dir=#{output_dir}" ]
1019
+
1020
+ # Include private & protected methods in documentation
1021
+ t.options << '--private' << '--protected'
1022
+
1023
+ # Handle readme if present
1024
+ if readme && File.exist?(readme)
1025
+ t.options << "--readme=#{readme}"
1026
+ end
1027
+
1028
+ # Add additional documentation files
1029
+ if doc_files&.any?
1030
+ t.files.concat(doc_files.flatten)
1031
+ end
1032
+
1033
+ # Add before hook for cleaning
1034
+ t.before = proc {
1035
+ clean output_dir
1036
+ puts "Generating full documentation in #{output_dir}..."
1037
+ }
1038
+ end
1039
+ end
1040
+
1041
+ # The yard_task method sets up and registers Rake tasks for generating and
1042
+ # managing YARD documentation.
1043
+ #
1044
+ # It creates multiple subtasks under the :yard namespace, including tasks for
1045
+ # creating private documentation, viewing the generated documentation,
1046
+ # cleaning up documentation files, and listing undocumented elements.
1047
+ # If YARD is not available, the method returns early without defining any tasks.
746
1048
  def yard_task
747
1049
  defined? YARD or return
1050
+ yard_doc_task
1051
+ desc 'Create yard documentation (including private)'
1052
+ task :doc => :yard_doc
748
1053
  namespace :yard do
749
1054
  my_yard_dir = Pathname.new(yard_dir)
750
1055
 
751
- desc 'Create yard documentation (including private)'
752
- task :private do
753
- sh "yardoc -o #{my_yard_dir}"
754
- end
1056
+ task :private => :yard_doc
1057
+
1058
+ task :public => :yard_doc
1059
+
1060
+ desc 'Create yard documentation'
1061
+ task :doc => :yard_doc
755
1062
 
756
1063
  desc 'View the yard documentation'
757
1064
  task :view do
@@ -789,7 +1096,6 @@ class GemHadar
789
1096
  gem_hadar_update_task
790
1097
  gemspec_task
791
1098
  gems_install_task
792
- doc_task
793
1099
  if test_dir
794
1100
  test_task
795
1101
  rcov_task
@@ -801,6 +1107,7 @@ class GemHadar
801
1107
  version_bump_task
802
1108
  version_tag_task
803
1109
  push_task
1110
+ release_task
804
1111
  write_ignore_file
805
1112
  write_gemfile
806
1113
  if extensions.full?
@@ -812,6 +1119,77 @@ class GemHadar
812
1119
  self
813
1120
  end
814
1121
 
1122
+ # The edit_temp_file method creates a temporary file with the provided
1123
+ # content, opens it in an editor for user modification, and returns the
1124
+ # updated content.
1125
+ #
1126
+ # @param content [ String ] the initial content to write to the temporary file
1127
+ def edit_temp_file(content)
1128
+ editor = ENV.fetch('EDITOR', `which vi`.chomp)
1129
+ unless File.exist?(editor)
1130
+ warn "Can't find EDITOR. => Returning."
1131
+ return
1132
+ end
1133
+ temp_file = Tempfile.new('changelog')
1134
+ temp_file.write(content)
1135
+ temp_file.close
1136
+
1137
+ unless system("#{editor} #{temp_file.path}")
1138
+ warn "#{editor} returned #{$?.exitstatus} => Returning."
1139
+ return
1140
+ end
1141
+
1142
+ File.read(temp_file.path)
1143
+ ensure
1144
+ temp_file&.close&.unlink
1145
+ end
1146
+
1147
+ # Generates a response from an AI model using the Ollama::Client.
1148
+ #
1149
+ # @param [String] system The system prompt for the AI model.
1150
+ # @param [String] prompt The user prompt to generate a response to.
1151
+ # @return [String, nil] The generated response or nil if generation fails.
1152
+ def ollama_generate(system:, prompt:)
1153
+ base_url = ENV['OLLAMA_URL']
1154
+ if base_url.blank? && host = ENV['OLLAMA_HOST'].full?
1155
+ base_url = 'http://%s' % host
1156
+ end
1157
+ base_url.present? or return
1158
+ ollama = Ollama::Client.new(base_url:, read_timeout: 600, connect_timeout: 60)
1159
+ model = ENV.fetch('OLLAMA_MODEL', 'llama3.1')
1160
+ options = ENV['OLLAMA_OPTIONS'].full? { |o| JSON.parse(o) } || {}
1161
+ options |= { "temperature" => 0, "top_p" => 1, "min_p" => 0.1 }
1162
+ ollama.generate(model:, system:, prompt:, options:, stream: false, think: false).response
1163
+ end
1164
+
1165
+ # Increases the specified part of the version number and writes it back to
1166
+ # the VERSION file.
1167
+ #
1168
+ # @param [Symbol, String] type The part of the version to bump (:major, :minor, or :build)
1169
+ def version_bump_to(type)
1170
+ type = type.to_sym
1171
+ version = File.read('VERSION').chomp.version
1172
+ version.bump(type)
1173
+ secure_write('VERSION') { |v| v.puts version }
1174
+ exit 0
1175
+ end
1176
+
1177
+ # Determine the start and end versions for diff comparison.
1178
+ #
1179
+ # If the VERSION env var is set, it will be used as the starting version tag.
1180
+ # Otherwise, it defaults to the current commit's version or the latest tag.
1181
+ #
1182
+ # @return [Array(String, String)] A fixed-size array containing:
1183
+ # - The start version (e.g., '1.2.3') from which changes are compared.
1184
+ # - The end version (e.g., '1.2.4' or 'HEAD') up to which changes are compared.
1185
+ def determine_version_range
1186
+ version_tags = versions.map { version_tag(_1) } + %w[ HEAD ]
1187
+ found_version_tag = version_tags.index(version_tag(version))
1188
+ found_version_tag.nil? and fail "cannot find version tag #{version_tag(version)}"
1189
+ start_version, end_version = version_tags[found_version_tag, 2]
1190
+ return start_version, end_version
1191
+ end
1192
+
815
1193
  # The write_ignore_file method writes the current ignore_files configuration
816
1194
  # to a .gitignore file in the project root directory.
817
1195
  def write_ignore_file
@@ -909,8 +1287,12 @@ class GemHadar
909
1287
  s.rdoc_options << '--title' << "#{name.camelize} - #{summary}"
910
1288
  end
911
1289
  if readme
912
- s.rdoc_options << '--main' << readme
913
- s.extra_rdoc_files << readme
1290
+ if File.exist?(readme)
1291
+ s.rdoc_options << '--main' << readme
1292
+ s.extra_rdoc_files << readme
1293
+ else
1294
+ warn "Add a #{readme} file to document your gem!"
1295
+ end
914
1296
  end
915
1297
  doc_files.full? { |df| s.extra_rdoc_files.concat Array(df) }
916
1298
  end
@@ -934,12 +1316,6 @@ class GemHadar
934
1316
  end
935
1317
  super(*args)
936
1318
  end
937
- def fail(*args)
938
- args.map! do |a|
939
- a.respond_to?(:to_str) ? color(196) { a.to_str } : a
940
- end
941
- super(*args)
942
- end
943
1319
 
944
1320
  # The git_remotes method retrieves the list of remote repositories configured
945
1321
  # for the current Git project.
@@ -999,19 +1375,33 @@ class GemHadar
999
1375
  #
1000
1376
  # @return [ Array<String> ] an array of version strings sorted in ascending
1001
1377
  # order according to semantic versioning rules.
1378
+ memoize method:
1002
1379
  def versions
1003
- @versions ||= `git tag`.lines.grep(/^v?\d+\.\d+\.\d+$/).map(&:chomp).map {
1380
+ `git tag`.lines.grep(/^v?\d+\.\d+\.\d+$/).map(&:chomp).map {
1004
1381
  _1.sub(/\Av/, '')
1005
1382
  }.sort_by(&:version)
1006
1383
  end
1007
1384
 
1008
1385
  # The version_tag method prepends a 'v' prefix to the given version
1009
- # string.
1386
+ # string, unless it's HEAD.
1010
1387
  #
1011
1388
  # @param version [String] the version string to modify
1012
1389
  # @return [String] the modified version string with a 'v' prefix
1013
1390
  def version_tag(version)
1014
- version.dup.prepend ?v
1391
+ if version != 'HEAD'
1392
+ version.dup.prepend ?v
1393
+ else
1394
+ version.dup
1395
+ end
1396
+ end
1397
+
1398
+ # The version_untag method removes the 'v' prefix from a version tag string.
1399
+ #
1400
+ # @param version_tag [ String ] the version tag string that may start with 'v'
1401
+ #
1402
+ # @return [ String ] the version string with the 'v' prefix removed
1403
+ def version_untag(version_tag)
1404
+ version.sub(/\Av/, '')
1015
1405
  end
1016
1406
 
1017
1407
  # The github_remote_url method retrieves and parses the GitHub remote URL