loading
Generated 2025-10-08T18:00:44-06:00

All Files ( 100.0% covered at 40.02 hits/line )

108 files in total.
1809 relevant lines, 1809 lines covered and 0 lines missed. ( 100.0% )
152 total branches, 152 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
lib/milestoner.rb 100.00 % 21 9 9 0 1.56 100.00 % 0 0 0
lib/milestoner/builders/adoc/container.rb 100.00 % 15 7 7 0 1.29 100.00 % 0 0 0
lib/milestoner/builders/adoc/dependencies.rb 100.00 % 9 4 4 0 1.00 100.00 % 0 0 0
lib/milestoner/builders/adoc/indexer.rb 100.00 % 28 14 14 0 3.50 100.00 % 0 0 0
lib/milestoner/builders/adoc/pager.rb 100.00 % 35 18 18 0 11.39 100.00 % 0 0 0
lib/milestoner/builders/ascii_doc.rb 100.00 % 44 26 26 0 3.23 100.00 % 0 0 0
lib/milestoner/builders/container.rb 100.00 % 19 11 11 0 1.55 100.00 % 0 0 0
lib/milestoner/builders/dependencies.rb 100.00 % 9 4 4 0 1.00 100.00 % 0 0 0
lib/milestoner/builders/feed.rb 100.00 % 35 19 19 0 2.32 100.00 % 0 0 0
lib/milestoner/builders/manifest.rb 100.00 % 41 22 22 0 4.36 100.00 % 2 2 0
lib/milestoner/builders/markdown.rb 100.00 % 44 26 26 0 3.23 100.00 % 0 0 0
lib/milestoner/builders/md/container.rb 100.00 % 15 7 7 0 1.29 100.00 % 0 0 0
lib/milestoner/builders/md/dependencies.rb 100.00 % 9 4 4 0 1.00 100.00 % 0 0 0
lib/milestoner/builders/md/indexer.rb 100.00 % 28 14 14 0 3.50 100.00 % 0 0 0
lib/milestoner/builders/md/pager.rb 100.00 % 35 18 18 0 8.72 100.00 % 0 0 0
lib/milestoner/builders/path_resolver.rb 100.00 % 23 12 12 0 68.17 100.00 % 4 4 0
lib/milestoner/builders/site/container.rb 100.00 % 16 8 8 0 1.38 100.00 % 0 0 0
lib/milestoner/builders/site/dependencies.rb 100.00 % 9 4 4 0 1.00 100.00 % 0 0 0
lib/milestoner/builders/site/indexer.rb 100.00 % 34 18 18 0 4.44 100.00 % 2 2 0
lib/milestoner/builders/site/pager.rb 100.00 % 35 18 18 0 11.28 100.00 % 0 0 0
lib/milestoner/builders/site/styler.rb 100.00 % 47 24 24 0 4.96 100.00 % 4 4 0
lib/milestoner/builders/stream.rb 100.00 % 40 21 21 0 4.10 100.00 % 0 0 0
lib/milestoner/builders/syndication/builder.rb 100.00 % 124 59 59 0 34.66 100.00 % 4 4 0
lib/milestoner/builders/syndication/indexer.rb 100.00 % 29 15 15 0 6.47 100.00 % 0 0 0
lib/milestoner/builders/syndication/refine.rb 100.00 % 27 19 19 0 1.74 100.00 % 0 0 0
lib/milestoner/builders/syndication/shared.rb 100.00 % 27 13 13 0 232.38 100.00 % 0 0 0
lib/milestoner/builders/web.rb 100.00 % 44 26 26 0 2.65 100.00 % 0 0 0
lib/milestoner/cli/actions/build/basename.rb 100.00 % 27 13 13 0 2.69 100.00 % 0 0 0
lib/milestoner/cli/actions/build/format.rb 100.00 % 24 11 11 0 3.00 100.00 % 0 0 0
lib/milestoner/cli/actions/build/index.rb 100.00 % 26 12 12 0 2.83 100.00 % 0 0 0
lib/milestoner/cli/actions/build/label.rb 100.00 % 24 11 11 0 3.00 100.00 % 0 0 0
lib/milestoner/cli/actions/build/layout.rb 100.00 % 28 13 13 0 2.92 100.00 % 2 2 0
lib/milestoner/cli/actions/build/manifest.rb 100.00 % 24 11 11 0 3.00 100.00 % 0 0 0
lib/milestoner/cli/actions/build/max.rb 100.00 % 24 11 11 0 3.00 100.00 % 0 0 0
lib/milestoner/cli/actions/build/output.rb 100.00 % 25 12 12 0 2.92 100.00 % 0 0 0
lib/milestoner/cli/actions/build/stylesheet.rb 100.00 % 26 12 12 0 2.83 100.00 % 0 0 0
lib/milestoner/cli/actions/build/tail.rb 100.00 % 26 12 12 0 2.83 100.00 % 0 0 0
lib/milestoner/cli/actions/build/version.rb 100.00 % 33 16 16 0 2.63 100.00 % 0 0 0
lib/milestoner/cli/actions/cache/create.rb 100.00 % 41 22 22 0 1.68 100.00 % 5 5 0
lib/milestoner/cli/actions/cache/delete.rb 100.00 % 31 14 14 0 1.50 100.00 % 0 0 0
lib/milestoner/cli/actions/cache/find.rb 100.00 % 31 14 14 0 1.21 100.00 % 0 0 0
lib/milestoner/cli/actions/cache/info.rb 100.00 % 29 14 14 0 1.29 100.00 % 2 2 0
lib/milestoner/cli/actions/cache/list.rb 100.00 % 41 21 21 0 1.71 100.00 % 2 2 0
lib/milestoner/cli/actions/next.rb 100.00 % 22 10 10 0 1.00 100.00 % 0 0 0
lib/milestoner/cli/actions/publish.rb 100.00 % 53 29 29 0 4.07 100.00 % 3 3 0
lib/milestoner/cli/commands/build.rb 100.00 % 49 29 29 0 3.24 100.00 % 2 2 0
lib/milestoner/cli/commands/cache.rb 100.00 % 24 13 13 0 1.00 100.00 % 0 0 0
lib/milestoner/cli/shell.rb 100.00 % 42 24 24 0 4.79 100.00 % 0 0 0
lib/milestoner/commits/categorizer.rb 100.00 % 50 23 23 0 40.83 100.00 % 4 4 0
lib/milestoner/commits/collector.rb 100.00 % 31 17 17 0 8.76 100.00 % 6 6 0
lib/milestoner/commits/enricher.rb 100.00 % 53 21 21 0 36.76 100.00 % 0 0 0
lib/milestoner/commits/enrichers/author.rb 100.00 % 26 12 12 0 13.08 100.00 % 0 0 0
lib/milestoner/commits/enrichers/body.rb 100.00 % 28 14 14 0 7.57 100.00 % 0 0 0
lib/milestoner/commits/enrichers/colleague.rb 100.00 % 33 16 16 0 7.88 100.00 % 0 0 0
lib/milestoner/commits/enrichers/container.rb 100.00 % 27 18 18 0 1.67 100.00 % 0 0 0
lib/milestoner/commits/enrichers/dependencies.rb 100.00 % 11 5 5 0 1.00 100.00 % 0 0 0
lib/milestoner/commits/enrichers/format.rb 100.00 % 23 11 11 0 1.36 100.00 % 0 0 0
lib/milestoner/commits/enrichers/issue.rb 100.00 % 30 16 16 0 9.38 100.00 % 0 0 0
lib/milestoner/commits/enrichers/milestone.rb 100.00 % 24 12 12 0 1.50 100.00 % 0 0 0
lib/milestoner/commits/enrichers/note.rb 100.00 % 28 14 14 0 7.57 100.00 % 0 0 0
lib/milestoner/commits/enrichers/review.rb 100.00 % 23 11 11 0 1.18 100.00 % 0 0 0
lib/milestoner/commits/enrichers/time.rb 100.00 % 20 9 9 0 1.33 100.00 % 0 0 0
lib/milestoner/commits/enrichers/uri.rb 100.00 % 14 6 6 0 1.00 100.00 % 0 0 0
lib/milestoner/commits/versioner.rb 100.00 % 56 30 30 0 47.73 100.00 % 2 2 0
lib/milestoner/configuration/contract.rb 100.00 % 72 56 56 0 1.02 100.00 % 0 0 0
lib/milestoner/configuration/model.rb 100.00 % 49 3 3 0 1.00 100.00 % 0 0 0
lib/milestoner/configuration/transformers/build/template_paths.rb 100.00 % 32 16 16 0 80.50 100.00 % 0 0 0
lib/milestoner/configuration/transformers/citations/description.rb 100.00 % 39 20 20 0 150.00 100.00 % 2 2 0
lib/milestoner/configuration/transformers/citations/label.rb 100.00 % 39 20 20 0 150.00 100.00 % 2 2 0
lib/milestoner/configuration/transformers/citations/uri.rb 100.00 % 45 23 23 0 168.30 100.00 % 4 4 0
lib/milestoner/configuration/transformers/gems/description.rb 100.00 % 37 20 20 0 150.00 100.00 % 2 2 0
lib/milestoner/configuration/transformers/gems/label.rb 100.00 % 37 20 20 0 150.00 100.00 % 2 2 0
lib/milestoner/configuration/transformers/gems/name.rb 100.00 % 37 20 20 0 107.70 100.00 % 2 2 0
lib/milestoner/configuration/transformers/gems/uri.rb 100.00 % 43 23 23 0 168.30 100.00 % 4 4 0
lib/milestoner/configuration/transformers/generator/label.rb 100.00 % 31 16 16 0 133.81 100.00 % 0 0 0
lib/milestoner/configuration/transformers/generator/uri.rb 100.00 % 31 16 16 0 133.81 100.00 % 0 0 0
lib/milestoner/configuration/transformers/generator/version.rb 100.00 % 31 16 16 0 133.81 100.00 % 0 0 0
lib/milestoner/configuration/transformers/project/author.rb 100.00 % 34 17 17 0 151.59 100.00 % 2 2 0
lib/milestoner/configuration/transformers/project/label.rb 100.00 % 21 11 11 0 78.64 100.00 % 0 0 0
lib/milestoner/configuration/transformers/project/version.rb 100.00 % 33 17 17 0 101.12 100.00 % 0 0 0
lib/milestoner/configuration/transformers/syndication/link.rb 100.00 % 33 16 16 0 241.06 100.00 % 0 0 0
lib/milestoner/container.rb 100.00 % 77 15 15 0 55.93 100.00 % 0 0 0
lib/milestoner/dependencies.rb 100.00 % 7 3 3 0 1.00 100.00 % 0 0 0
lib/milestoner/durationer.rb 100.00 % 32 13 13 0 195.62 100.00 % 4 4 0
lib/milestoner/models/commit.rb 100.00 % 55 12 12 0 48.58 100.00 % 0 0 0
lib/milestoner/models/link.rb 100.00 % 12 5 5 0 98.80 100.00 % 0 0 0
lib/milestoner/models/tag.rb 100.00 % 8 3 3 0 1.00 100.00 % 0 0 0
lib/milestoner/models/user.rb 100.00 % 12 5 5 0 246.00 100.00 % 0 0 0
lib/milestoner/renderers/asciidoc.rb 100.00 % 29 11 11 0 1.36 100.00 % 0 0 0
lib/milestoner/renderers/markdown.rb 100.00 % 36 14 14 0 1.43 100.00 % 0 0 0
lib/milestoner/renderers/universal.rb 100.00 % 28 14 14 0 2.71 100.00 % 0 0 0
lib/milestoner/sanitizer.rb 100.00 % 40 16 16 0 48.88 100.00 % 0 0 0
lib/milestoner/tags/builder.rb 100.00 % 40 22 22 0 5.45 100.00 % 0 0 0
lib/milestoner/tags/creator.rb 100.00 % 47 25 25 0 4.68 100.00 % 4 4 0
lib/milestoner/tags/enricher.rb 100.00 % 119 57 57 0 14.25 100.00 % 12 12 0
lib/milestoner/tags/manifest.rb 100.00 % 51 27 27 0 3.44 100.00 % 2 2 0
lib/milestoner/tags/publisher.rb 100.00 % 34 19 19 0 4.37 100.00 % 0 0 0
lib/milestoner/tags/pusher.rb 100.00 % 39 21 21 0 2.24 100.00 % 4 4 0
lib/milestoner/views/context.rb 100.00 % 48 16 16 0 3.75 100.00 % 0 0 0
lib/milestoner/views/milestones/index.rb 100.00 % 20 11 11 0 1.00 100.00 % 0 0 0
lib/milestoner/views/milestones/show.rb 100.00 % 22 12 12 0 1.00 100.00 % 0 0 0
lib/milestoner/views/parts/commit.rb 100.00 % 118 66 66 0 83.61 100.00 % 38 38 0
lib/milestoner/views/parts/tag.rb 100.00 % 81 44 44 0 68.34 100.00 % 12 12 0
lib/milestoner/views/parts/user.rb 100.00 % 32 15 15 0 276.00 100.00 % 4 4 0
lib/milestoner/views/scopes/content.rb 100.00 % 17 8 8 0 33.63 100.00 % 2 2 0
lib/milestoner/views/scopes/logo.rb 100.00 % 15 7 7 0 27.14 100.00 % 2 2 0
lib/milestoner/views/scopes/tag_signature.rb 100.00 % 27 13 13 0 8.00 100.00 % 2 2 0
lib/milestoner/views/scopes/users.rb 100.00 % 14 6 6 0 44.33 100.00 % 2 2 0

lib/milestoner.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "zeitwerk"
  3. 1 Zeitwerk::Loader.new.then do |loader|
  4. 1 loader.inflector.inflect "adoc" => "ADoc",
  5. "ascii_doc" => "ASCIIDoc",
  6. "cli" => "CLI",
  7. "md" => "MD",
  8. "uri" => "URI"
  9. 1 loader.tag = File.basename __FILE__, ".rb"
  10. 1 loader.push_dir __dir__
  11. 1 loader.setup
  12. end
  13. # Main namespace.
  14. 1 module Milestoner
  15. 1 def self.loader registry = Zeitwerk::Registry
  16. 6 @loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
  17. end
  18. end

lib/milestoner/builders/adoc/container.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module ADoc
  5. # Defines ASCII Doc dependencies.
  6. 1 module Container
  7. 1 extend Containable
  8. 2 register(:indexer) { Indexer.new }
  9. 2 register(:pager) { Pager.new }
  10. end
  11. end
  12. end
  13. end

lib/milestoner/builders/adoc/dependencies.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module ADoc
  5. 1 Dependencies = Infusible[Container]
  6. end
  7. end
  8. end

lib/milestoner/builders/adoc/indexer.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module ADoc
  5. # Builds ASCII Doc index.
  6. 1 class Indexer
  7. 1 include Milestoner::Dependencies[:settings, :logger]
  8. 1 def initialize(path_resolver: PathResolver, view: Views::Milestones::Index.new, **)
  9. 6 super(**)
  10. 6 @path_resolver = path_resolver
  11. 6 @view = view
  12. end
  13. 1 def call tags
  14. 11 path_resolver.call settings.build_output.join("index.adoc"), logger: do |path|
  15. 11 path.write view.call(tags:, layout: settings.build_layout, format: :adoc)
  16. end
  17. end
  18. 1 private
  19. 1 attr_reader :path_resolver, :view
  20. end
  21. end
  22. end
  23. end

lib/milestoner/builders/adoc/pager.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module ADoc
  5. # Builds ASCII Doc version.
  6. 1 class Pager
  7. 1 include Milestoner::Dependencies[:settings, :logger]
  8. 1 def initialize(path_resolver: PathResolver, view: Views::Milestones::Show.new, **)
  9. 20 super(**)
  10. 20 @path_resolver = path_resolver
  11. 20 @view = view
  12. end
  13. 1 def call past, tag, future
  14. 27 settings.project_version = tag.version
  15. 27 write past, tag, future
  16. end
  17. 1 private
  18. 1 attr_reader :path_resolver, :view
  19. 1 def write past, tag, future
  20. 27 path = settings.build_output.join(tag.version, settings.build_basename).sub_ext ".adoc"
  21. 27 path_resolver.call path, logger: do
  22. 27 path.write view.call(past:, tag:, future:, layout: settings.build_layout, format: :adoc)
  23. end
  24. end
  25. end
  26. end
  27. end
  28. end

lib/milestoner/builders/ascii_doc.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/array"
  3. 1 require "refinements/pathname"
  4. 1 module Milestoner
  5. 1 module Builders
  6. # Builds ASCII Doc files.
  7. 1 class ASCIIDoc
  8. 1 include Milestoner::Dependencies[:settings, :logger]
  9. 1 include ADoc::Dependencies[:indexer, :pager]
  10. 1 using Refinements::Array
  11. 1 using Refinements::Pathname
  12. 1 def initialize(tagger: Tags::Enricher.new, view: Views::Milestones::Show.new, **)
  13. 7 super(**)
  14. 7 @tagger = tagger
  15. 7 @view = view
  16. end
  17. 1 def call
  18. 8 tagger.call
  19. 6 .fmap { |tags| build tags }
  20. 2 .alt_map { |message| failure message }
  21. end
  22. 1 private
  23. 1 attr_reader :tagger, :view
  24. 1 def build tags
  25. 6 indexer.call tags
  26. 14 tags.ring { |future, present, past| pager.call past, present, future }
  27. 6 settings.build_output
  28. end
  29. 1 def failure message
  30. 4 logger.error { message }
  31. 2 message
  32. end
  33. end
  34. end
  35. end

lib/milestoner/builders/container.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "containable"
  3. 1 module Milestoner
  4. 1 module Builders
  5. # Registers all builders for injection.
  6. 1 module Container
  7. 1 extend Containable
  8. 2 register(:ascii_doc) { ASCIIDoc.new }
  9. 2 register(:feed) { Feed.new }
  10. 2 register(:manifest) { Manifest.new }
  11. 2 register(:markdown) { Markdown.new }
  12. 2 register(:stream) { Stream.new }
  13. 2 register(:web) { Web.new }
  14. end
  15. end
  16. end

lib/milestoner/builders/dependencies.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "infusible"
  3. 1 module Milestoner
  4. 1 module Builders
  5. 1 Dependencies = Infusible[Container]
  6. end
  7. end

lib/milestoner/builders/feed.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/pathname"
  3. 1 module Milestoner
  4. 1 module Builders
  5. # Builds syndicated feed output.
  6. 1 class Feed
  7. 1 include Milestoner::Dependencies[:logger]
  8. 1 using Refinements::Pathname
  9. 1 def initialize(tagger: Tags::Enricher.new, indexer: Syndication::Indexer.new, **)
  10. 5 super(**)
  11. 5 @tagger = tagger
  12. 5 @indexer = indexer
  13. end
  14. 1 def call
  15. 6 tagger.call
  16. 4 .bind { |tags| indexer.call tags }
  17. 2 .alt_map { |message| failure message }
  18. end
  19. 1 private
  20. 1 attr_reader :tagger, :indexer
  21. 1 def failure message
  22. 4 logger.error { message }
  23. 2 message
  24. end
  25. end
  26. end
  27. end

lib/milestoner/builders/manifest.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Builders
  5. # Builds JSON manifest.
  6. 1 class Manifest
  7. 1 include Dry::Monads[:result]
  8. 1 include Milestoner::Dependencies[:settings, :git, :logger]
  9. 1 def initialize(writer: Tags::Manifest.new, path_resolver: PathResolver, **)
  10. 10 super(**)
  11. 10 @writer = writer
  12. 10 @path_resolver = path_resolver
  13. end
  14. 1 def call
  15. 21 else: 7 then: 14 return Success writer.build_path unless settings.build_manifest
  16. 14 git.tags.either -> tags { write tags }, -> message { failure message }
  17. end
  18. 1 private
  19. 1 attr_reader :writer, :path_resolver
  20. 1 def write tags
  21. 5 path_resolver.call writer.build_path, logger: do
  22. 4 versions = tags.map(&:version)
  23. 4 writer.write latest: versions.last, versions:
  24. end
  25. end
  26. 1 def failure message
  27. 4 logger.error { message }
  28. 2 Failure message
  29. end
  30. end
  31. end
  32. end

lib/milestoner/builders/markdown.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/array"
  3. 1 require "refinements/pathname"
  4. 1 module Milestoner
  5. 1 module Builders
  6. # Builds Markdown files.
  7. 1 class Markdown
  8. 1 include Milestoner::Dependencies[:settings, :logger]
  9. 1 include MD::Dependencies[:indexer, :pager]
  10. 1 using Refinements::Array
  11. 1 using Refinements::Pathname
  12. 1 def initialize(tagger: Tags::Enricher.new, view: Views::Milestones::Show.new, **)
  13. 7 super(**)
  14. 7 @tagger = tagger
  15. 7 @view = view
  16. end
  17. 1 def call
  18. 8 tagger.call
  19. 6 .fmap { |tags| build tags }
  20. 2 .alt_map { |message| failure message }
  21. end
  22. 1 private
  23. 1 attr_reader :tagger, :view
  24. 1 def build tags
  25. 6 indexer.call tags
  26. 14 tags.ring { |future, present, past| pager.call past, present, future }
  27. 6 settings.build_output
  28. end
  29. 1 def failure message
  30. 4 logger.error { message }
  31. 2 message
  32. end
  33. end
  34. end
  35. end

lib/milestoner/builders/md/container.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module MD
  5. # Defines Markdown dependencies.
  6. 1 module Container
  7. 1 extend Containable
  8. 2 register(:indexer) { Indexer.new }
  9. 2 register(:pager) { Pager.new }
  10. end
  11. end
  12. end
  13. end

lib/milestoner/builders/md/dependencies.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module MD
  5. 1 Dependencies = Infusible[Container]
  6. end
  7. end
  8. end

lib/milestoner/builders/md/indexer.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module MD
  5. # Builds Markdown index.
  6. 1 class Indexer
  7. 1 include Milestoner::Dependencies[:settings, :logger]
  8. 1 def initialize(path_resolver: PathResolver, view: Views::Milestones::Index.new, **)
  9. 6 super(**)
  10. 6 @path_resolver = path_resolver
  11. 6 @view = view
  12. end
  13. 1 def call tags
  14. 11 path_resolver.call settings.build_output.join("index.md"), logger: do |path|
  15. 11 path.write view.call(tags:, layout: settings.build_layout, format: :md)
  16. end
  17. end
  18. 1 private
  19. 1 attr_reader :path_resolver, :view
  20. end
  21. end
  22. end
  23. end

lib/milestoner/builders/md/pager.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module MD
  5. # Builds Markdown version.
  6. 1 class Pager
  7. 1 include Milestoner::Dependencies[:settings, :logger]
  8. 1 def initialize(path_resolver: PathResolver, view: Views::Milestones::Show.new, **)
  9. 14 super(**)
  10. 14 @path_resolver = path_resolver
  11. 14 @view = view
  12. end
  13. 1 def call past, tag, future
  14. 21 settings.project_version = tag.version
  15. 21 write past, tag, future
  16. end
  17. 1 private
  18. 1 attr_reader :path_resolver, :view
  19. 1 def write past, tag, future
  20. 21 path = settings.build_output.join(tag.version, settings.build_basename).sub_ext ".md"
  21. 21 path_resolver.call path, logger: do
  22. 21 path.write view.call(past:, tag:, future:, layout: settings.build_layout, format: :md)
  23. end
  24. end
  25. end
  26. end
  27. end
  28. end

lib/milestoner/builders/path_resolver.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/pathname"
  4. 1 module Milestoner
  5. # Safely handles file paths which may or may not exist.
  6. 1 module Builders
  7. 1 using Refinements::Pathname
  8. 1 PathResolver = lambda do |path, logger:, &block|
  9. 136 then: 2 if path.exist?
  10. 4 logger.warn { "Path exists: #{path}. Skipped." }
  11. else: 134 else
  12. 134 path.make_ancestors
  13. 134 then: 131 else: 3 block.call path if block
  14. 268 logger.info { "Created: #{path}." }
  15. end
  16. 136 Dry::Monads::Success path
  17. end
  18. end
  19. end

lib/milestoner/builders/site/container.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module Site
  5. # Defines web dependencies.
  6. 1 module Container
  7. 1 extend Containable
  8. 2 register(:indexer) { Indexer.new }
  9. 2 register(:pager) { Pager.new }
  10. 2 register(:styler) { Styler.new }
  11. end
  12. end
  13. end
  14. end

lib/milestoner/builders/site/dependencies.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module Site
  5. 1 Dependencies = Infusible[Container]
  6. end
  7. end
  8. end

lib/milestoner/builders/site/indexer.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Builders
  5. 1 module Site
  6. # Builds web index.
  7. 1 class Indexer
  8. 1 include Dry::Monads[:result]
  9. 1 include Milestoner::Dependencies[:settings, :logger]
  10. 1 def initialize(path_resolver: PathResolver, view: Views::Milestones::Index.new, **)
  11. 9 super(**)
  12. 9 @path_resolver = path_resolver
  13. 9 @view = view
  14. end
  15. 1 def call tags
  16. 12 else: 10 then: 2 return Success() unless settings.build_index
  17. 10 path_resolver.call settings.build_output.join("index.html"), logger: do |path|
  18. 10 settings.project_version = nil
  19. 10 path.write view.call(tags:, layout: settings.build_layout)
  20. end
  21. end
  22. 1 private
  23. 1 attr_reader :path_resolver, :view
  24. end
  25. end
  26. end
  27. end

lib/milestoner/builders/site/pager.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module Site
  5. # Builds web version.
  6. 1 class Pager
  7. 1 include Milestoner::Dependencies[:settings, :logger]
  8. 1 def initialize(path_resolver: PathResolver, view: Views::Milestones::Show.new, **)
  9. 21 super(**)
  10. 21 @path_resolver = path_resolver
  11. 21 @view = view
  12. end
  13. 1 def call past, tag, future
  14. 26 settings.project_version = tag.version
  15. 26 write past, tag, future
  16. end
  17. 1 private
  18. 1 attr_reader :path_resolver, :view
  19. 1 def write past, tag, future
  20. 26 path = settings.build_output.join(tag.version, settings.build_basename).sub_ext ".html"
  21. 26 path_resolver.call path, logger: do
  22. 26 path.write view.call(past:, tag:, future:, layout: settings.build_layout)
  23. end
  24. end
  25. end
  26. end
  27. end
  28. end

lib/milestoner/builders/site/styler.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/pathname"
  4. 1 module Milestoner
  5. 1 module Builders
  6. 1 module Site
  7. # Builds web stylesheet.
  8. 1 class Styler
  9. 1 include Milestoner::Dependencies[:settings, :logger]
  10. 1 include Dry::Monads[:result]
  11. 1 using Refinements::Pathname
  12. 1 def initialize(path_resolver: PathResolver, **)
  13. 7 super(**)
  14. 7 @path_resolver = path_resolver
  15. end
  16. 1 def call
  17. 10 else: 8 then: 2 return Success() unless settings.build_stylesheet
  18. 8 path = build_path
  19. 16 path_resolver.call(path, logger:) { copy path }
  20. end
  21. 1 private
  22. 1 attr_reader :path_resolver
  23. 1 def build_path
  24. 8 path = Pathname settings.stylesheet_path
  25. 8 then: 1 else: 7 path.absolute? ? path : settings.build_output.join(path)
  26. end
  27. 1 def copy build_path
  28. 8 settings.build_template_paths
  29. 32 .map { |template| template.join "public/page.css.erb" }
  30. .find(&:exist?)
  31. .copy build_path.make_ancestors
  32. end
  33. end
  34. end
  35. end
  36. end

lib/milestoner/builders/stream.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. # Builds I/O stream output.
  5. 1 class Stream
  6. 1 include Milestoner::Dependencies[:settings, :logger, :io]
  7. 1 def initialize(tagger: Tags::Enricher.new, view: Views::Milestones::Show.new, **)
  8. 9 super(**)
  9. 9 @tagger = tagger
  10. 9 @view = view
  11. end
  12. 1 def call
  13. 9 tagger.call
  14. 7 .fmap { |tags| write tags }
  15. 2 .alt_map { |message| failure message }
  16. end
  17. 1 private
  18. 1 attr_reader :tagger, :view
  19. 8 def write(tags) = build(tags).tap { |content| io.write content }
  20. 1 def build tags
  21. 16 tags.reduce([]) { |content, tag| content.append render(tag) }
  22. .join(%(\n#{"-" * 80}\n\n))
  23. end
  24. 1 def render(tag) = view.call tag:, layout: settings.build_layout, format: :stream
  25. 1 def failure message
  26. 4 logger.error { message }
  27. 2 message
  28. end
  29. end
  30. end
  31. end

lib/milestoner/builders/syndication/builder.rb

100.0% lines covered

100.0% branches covered

59 relevant lines. 59 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 require "rss"
  5. 1 module Milestoner
  6. 1 module Builders
  7. 1 module Syndication
  8. # Builds feed in Atom format.
  9. 1 class Builder
  10. 1 include Milestoner::Dependencies[:settings]
  11. 1 include Dry::Monads[:result]
  12. 1 using Refine
  13. 1 def self.authors_for tags
  14. 123 tags.flat_map { |tag| tag.commits.map(&:author) }
  15. 44 then: 41 else: 3 .then { |users| users.any? ? users : tags.map(&:author) }
  16. .uniq
  17. end
  18. 1 def initialize(client: RSS::Maker, view: Views::Milestones::Show.new, **)
  19. 44 super(**)
  20. 44 @client = client
  21. 44 @view = view
  22. end
  23. 1 def call tags
  24. 43 then: 1 else: 42 return Failure "No tags or commits." if tags.empty?
  25. 42 Success build_feed(tags).to_s
  26. rescue NoMethodError, RSS::Error => error
  27. 2 Failure "#{self.class}: #{error.message.capitalize}."
  28. end
  29. 1 private
  30. 1 attr_reader :client, :view
  31. 1 def build_feed tags
  32. 42 client.make "atom" do |node|
  33. 42 build_channel node.channel, tags
  34. 41 build_tags node, tags
  35. end
  36. end
  37. 1 def build_channel node, tags
  38. 42 at = tags.first.committed_at
  39. 42 node.merge id: settings.project_uri_home,
  40. title: settings.syndication_label,
  41. subtitle: settings.project_description,
  42. icon: settings.project_uri_icon,
  43. logo: settings.project_uri_logo,
  44. rights: at.strftime("%Y"),
  45. updated: at
  46. 41 build_channel_elements node, tags
  47. end
  48. 1 def build_channel_elements node, tags
  49. 41 build_links node
  50. 41 build_generator node
  51. 41 build_authors node, self.class.authors_for(tags)
  52. 41 node.categories.build label: settings.project_label, term: settings.project_name
  53. end
  54. 1 def build_links node
  55. 41 node.links.build_for settings.syndication_links,
  56. label: :title,
  57. uri: :href,
  58. relation: :rel,
  59. mime: :type
  60. end
  61. 1 def build_generator node
  62. 41 node.generator do |generator|
  63. 41 generator.merge content: settings.generator_label,
  64. version: settings.generator_version,
  65. uri: settings.generator_uri
  66. end
  67. end
  68. 1 def build_tags node, tags = Core::EMPTY_ARRAY
  69. 116 tags.each { |tag| build_item node.items, tag }
  70. end
  71. 1 def build_item node, tag
  72. 75 node.new_item do |item|
  73. 75 build_item_metadata item, tag
  74. 75 build_item_content item.content, tag
  75. 75 build_authors item, tag.commits.map(&:author).uniq.select(&:name)
  76. 75 item.categories.build_for settings.syndication_categories, label: :label, name: :term
  77. end
  78. end
  79. 1 def build_item_metadata node, tag
  80. 75 committed_at = tag.committed_at
  81. 75 version = tag.version
  82. 75 node.merge id: format(settings.syndication_id, id: version),
  83. title: format(settings.syndication_entry_label, id: version),
  84. link: format(settings.syndication_entry_uri, id: "#{version}/"),
  85. rights: committed_at.strftime("%Y"),
  86. published: committed_at,
  87. updated: committed_at
  88. end
  89. 1 def build_item_content node, tag
  90. 75 content = view.call tag:, layout: settings.build_layout, format: :xml
  91. 75 node.merge content:, type: :html
  92. end
  93. 1 def build_authors node, users = Core::EMPTY_ARRAY
  94. 116 users.each do |user|
  95. 115 node.authors.build name: user.name, uri: format(settings.profile_uri, id: user.handle)
  96. end
  97. end
  98. end
  99. end
  100. end
  101. end

lib/milestoner/builders/syndication/indexer.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module Syndication
  5. # Builds syndicated feed output.
  6. 1 class Indexer
  7. 1 include Milestoner::Dependencies[:settings, :logger]
  8. 1 def initialize(path_resolver: PathResolver, syndicator: Syndication::Builder.new, **)
  9. 13 super(**)
  10. 13 @path_resolver = path_resolver
  11. 13 @syndicator = syndicator
  12. end
  13. 13 def call(tags) = syndicator.call(tags).bind { |body| write body }
  14. 1 private
  15. 1 attr_reader :path_resolver, :syndicator
  16. 1 def write body
  17. 12 path = settings.build_output.join(settings.build_basename).sub_ext(".xml")
  18. 24 path_resolver.call(path, logger:) { path.write body }
  19. end
  20. end
  21. end
  22. end
  23. end

lib/milestoner/builders/syndication/refine.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "rss"
  3. 1 module Milestoner
  4. 1 module Builders
  5. 1 module Syndication
  6. # Smooths out the rough edges of the RSS gem object which are harder to work with.
  7. 1 module Refine
  8. 2 refine(RSS::Maker::Atom::Feed::Channel) { import_methods Shared }
  9. 2 refine(RSS::Maker::Atom::Feed::Channel::Authors) { import_methods Shared }
  10. 2 refine(RSS::Maker::Atom::Feed::Channel::Authors::Author) { import_methods Shared }
  11. 2 refine(RSS::Maker::Atom::Feed::Channel::Categories) { import_methods Shared }
  12. 2 refine(RSS::Maker::Atom::Feed::Channel::Categories::Category) { import_methods Shared }
  13. 2 refine(RSS::Maker::Atom::Feed::Channel::Generator) { import_methods Shared }
  14. 2 refine(RSS::Maker::Atom::Feed::Channel::Links::Link) { import_methods Shared }
  15. 2 refine(RSS::Maker::Atom::Feed::Items::Item) { import_methods Shared }
  16. 2 refine(RSS::Maker::Atom::Feed::Channel::Links) { import_methods Shared }
  17. 2 refine(RSS::Maker::Atom::Feed::Items::Item::Authors) { import_methods Shared }
  18. 2 refine(RSS::Maker::Atom::Feed::Items::Item::Authors::Author) { import_methods Shared }
  19. 2 refine(RSS::Maker::Atom::Feed::Items::Item::Categories) { import_methods Shared }
  20. 2 refine(RSS::Maker::Atom::Feed::Items::Item::Categories::Category) { import_methods Shared }
  21. 2 refine(RSS::Maker::Atom::Feed::Items::Item::Content) { import_methods Shared }
  22. end
  23. end
  24. end
  25. end

lib/milestoner/builders/syndication/shared.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Builders
  4. 1 module Syndication
  5. # Provides shared functionality for refinements.
  6. 1 module Shared
  7. 1011 def merge(**attributes) = attributes.each { |key, value| public_send :"#{key}=", value }
  8. 1 def build_for(collection, **)
  9. 273 collection.each { |attributes| build(**attributes.transform_keys(**)) }
  10. end
  11. 1 def build(**attributes)
  12. 313 node = public_send :"new_#{kind}"
  13. 1103 attributes.each { |key, value| node.public_send :"#{key}=", value }
  14. end
  15. 1 private
  16. 1 def kind
  17. 313 self.class.name.downcase.split("::").last.sub("categories", "category").delete_suffix("s")
  18. end
  19. end
  20. end
  21. end
  22. end

lib/milestoner/builders/web.rb

100.0% lines covered

100.0% branches covered

26 relevant lines. 26 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/array"
  3. 1 require "refinements/pathname"
  4. 1 module Milestoner
  5. 1 module Builders
  6. # Builds web files,
  7. 1 class Web
  8. 1 include Milestoner::Dependencies[:settings, :logger]
  9. 1 include Site::Dependencies[:indexer, :pager, :styler]
  10. 1 using Refinements::Array
  11. 1 using Refinements::Pathname
  12. 1 def initialize(tagger: Tags::Enricher.new, **)
  13. 7 super(**)
  14. 7 @tagger = tagger
  15. end
  16. 1 def call
  17. 6 tagger.call
  18. 4 .fmap { |tags| build tags }
  19. 2 .alt_map { |message| failure message }
  20. end
  21. 1 private
  22. 1 attr_reader :tagger
  23. 1 def build tags
  24. 4 styler.call
  25. 4 indexer.call tags
  26. 10 tags.ring { |future, present, past| pager.call past, present, future }
  27. 4 settings.build_output
  28. end
  29. 1 def failure message
  30. 4 logger.error { message }
  31. 2 message
  32. end
  33. end
  34. end
  35. end

lib/milestoner/cli/actions/build/basename.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "pathname"
  3. 1 require "sod"
  4. 1 module Milestoner
  5. 1 module CLI
  6. 1 module Actions
  7. 1 module Build
  8. # Handles output path.
  9. 1 class Basename < Sod::Action
  10. 1 include Dependencies[:settings]
  11. 1 description "Set basename."
  12. 1 ancillary "The file extension is dynamically calculated from format."
  13. 1 on %w[-b --basename], argument: "[NAME]"
  14. 23 default { Container[:settings].build_basename }
  15. 1 def call(name = default) = settings.build_basename = name
  16. end
  17. end
  18. end
  19. end
  20. end

lib/milestoner/cli/actions/build/format.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Build
  7. # Handles build output format.
  8. 1 class Format < Sod::Action
  9. 1 include Dependencies[:settings]
  10. 1 description "Set output format."
  11. 1 on %w[-f --format], argument: "[KIND]", allow: %w[ascii_doc feed markdown stream web]
  12. 23 default { Container[:settings].build_format }
  13. 1 def call(kind = default) = settings.build_format = kind
  14. end
  15. end
  16. end
  17. end
  18. end

lib/milestoner/cli/actions/build/index.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Build
  7. # Handles build index.
  8. 1 class Index < Sod::Action
  9. 1 include Dependencies[:settings]
  10. 1 description "Enable/disable versions index."
  11. 1 ancillary "Only used by web format."
  12. 1 on "--[no-]index"
  13. 23 default { Container[:settings].build_index }
  14. 1 def call(boolean) = settings.build_index = boolean
  15. end
  16. end
  17. end
  18. end
  19. end

lib/milestoner/cli/actions/build/label.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Build
  7. # Handles build label.
  8. 1 class Label < Sod::Action
  9. 1 include Dependencies[:settings]
  10. 1 description "Set label."
  11. 1 on %w[-l --label], argument: "[TEXT]"
  12. 23 default { Container[:settings].project_label }
  13. 1 def call(label = default) = settings.project_label = label
  14. end
  15. end
  16. end
  17. end
  18. end

lib/milestoner/cli/actions/build/layout.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Build
  7. # Handles build layout.
  8. 1 class Layout < Sod::Action
  9. 1 include Dependencies[:settings]
  10. 1 description "Set view template layout."
  11. 1 ancillary "Use false to disable."
  12. 1 on %w[-L --layout], argument: "[NAME]"
  13. 24 default { Container[:settings].build_layout }
  14. 1 def call layout = default
  15. 3 then: 1 else: 2 settings.build_layout = layout == "false" ? false : layout
  16. end
  17. end
  18. end
  19. end
  20. end
  21. end

lib/milestoner/cli/actions/build/manifest.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Build
  7. # Handles build manifest.
  8. 1 class Manifest < Sod::Action
  9. 1 include Dependencies[:settings]
  10. 1 description "Enable/disable manifest."
  11. 1 on "--[no-]manifest"
  12. 23 default { Container[:settings].build_manifest }
  13. 1 def call(boolean) = settings.build_manifest = boolean
  14. end
  15. end
  16. end
  17. end
  18. end

lib/milestoner/cli/actions/build/max.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Build
  7. # Handles build maximum.
  8. 1 class Max < Sod::Action
  9. 1 include Dependencies[:settings]
  10. 1 description "Set maximum number of tags to process."
  11. 1 on %w[-m --max], argument: "[NUMBER]", type: Integer
  12. 23 default { Container[:settings].build_max }
  13. 1 def call(max = default) = settings.build_max = max
  14. end
  15. end
  16. end
  17. end
  18. end

lib/milestoner/cli/actions/build/output.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "pathname"
  3. 1 require "sod"
  4. 1 module Milestoner
  5. 1 module CLI
  6. 1 module Actions
  7. 1 module Build
  8. # Handles build output path.
  9. 1 class Output < Sod::Action
  10. 1 include Dependencies[:settings]
  11. 1 description "Set output path."
  12. 1 on %w[-o --output], argument: "[PATH]"
  13. 24 default { Container[:settings].build_output }
  14. 1 def call(path = default) = settings.build_output = Pathname(path).expand_path
  15. end
  16. end
  17. end
  18. end
  19. end

lib/milestoner/cli/actions/build/stylesheet.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Build
  7. # Handles build stylesheet.
  8. 1 class Stylesheet < Sod::Action
  9. 1 include Dependencies[:settings]
  10. 1 description "Enable/disable stylesheet."
  11. 1 ancillary "Only used by web format."
  12. 1 on "--[no-]stylesheet"
  13. 23 default { Container[:settings].build_stylesheet }
  14. 1 def call(boolean) = settings.build_stylesheet = boolean
  15. end
  16. end
  17. end
  18. end
  19. end

lib/milestoner/cli/actions/build/tail.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Build
  7. # Handles build tail.
  8. 1 class Tail < Sod::Action
  9. 1 include Dependencies[:settings]
  10. 1 description "Set tail reference."
  11. 1 ancillary "Defines the Git reference at which to cap the build."
  12. 1 on %w[-t --tail], argument: "[REFERENCE]", allow: %w[head tag]
  13. 23 default { Container[:settings].build_tail }
  14. 1 def call(reference = default) = settings.build_tail = reference
  15. end
  16. end
  17. end
  18. end
  19. end

lib/milestoner/cli/actions/build/version.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 require "versionaire"
  4. 1 module Milestoner
  5. 1 module CLI
  6. 1 module Actions
  7. 1 module Build
  8. # Handles build version.
  9. 1 class Version < Sod::Action
  10. 1 include Dependencies[:settings, :logger]
  11. 1 using Versionaire::Cast
  12. 1 description "Set version."
  13. 1 ancillary "Calculated from commit trailers when not supplied."
  14. 1 on %w[-v --version], argument: "[VERSION]"
  15. 24 default { Container[:settings].project_version }
  16. 1 def call version = default
  17. 3 settings.project_version = Version version
  18. rescue Versionaire::Error => error
  19. 2 logger.error { error.message }
  20. end
  21. end
  22. end
  23. end
  24. end
  25. end

lib/milestoner/cli/actions/cache/create.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
5 total branches, 5 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "sod"
  4. 1 module Milestoner
  5. 1 module CLI
  6. 1 module Actions
  7. 1 module Cache
  8. # Handles creating or updating a user within the cache.
  9. 1 class Create < Sod::Action
  10. 1 include Dependencies[:logger, client: :cache]
  11. 1 description "Create user."
  12. 1 ancillary %(Example: "1,zoe,Zoë Washburne".)
  13. 1 on %w[-c --create], argument: "external_id,handle,name"
  14. 1 def call values
  15. 6 case values.split ","
  16. in: 2 in String => external_id, String => handle, String => name
  17. 4 client.write(:users) { upsert({external_id:, handle:, name:}) }
  18. 2 in: 1 .bind { |user| log_info "Created: #{user.name.inspect}" }
  19. 1 in: 1 in String, String then log_error "Name must be supplied."
  20. 1 in: 1 in [String] then log_error "Handle and Name must be supplied."
  21. 1 else: 1 in Core::EMPTY_ARRAY then log_error "No values given."
  22. 1 else log_error "Too many values given."
  23. end
  24. end
  25. 1 private
  26. 3 def log_info(message) = logger.info { message }
  27. 5 def log_error(message) = logger.error { message }
  28. end
  29. end
  30. end
  31. end
  32. end

lib/milestoner/cli/actions/cache/delete.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Cache
  7. # Handles deleting a user from the cache.
  8. 1 class Delete < Sod::Action
  9. 1 include Dependencies[:logger, client: :cache]
  10. 1 description "Delete user."
  11. 1 on %w[-d --delete], argument: "NAME"
  12. 1 def call name
  13. 6 client.write(:users) { delete name }
  14. .either(method(:success), method(:failure))
  15. end
  16. 1 private
  17. 3 def success(user) = logger.info { "Deleted: #{user.name.inspect}." }
  18. 1 def failure(message) = logger.abort message
  19. end
  20. end
  21. end
  22. end
  23. end

lib/milestoner/cli/actions/cache/find.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Cache
  7. # Handles finding a user in the cache.
  8. 1 class Find < Sod::Action
  9. 1 include Dependencies[:logger, :io, client: :cache]
  10. 1 description "Find user."
  11. 1 on %w[-f --find], argument: "NAME"
  12. 1 def call name
  13. 4 client.read(:users) { find name }
  14. .either(method(:success), method(:failure))
  15. end
  16. 1 private
  17. 1 def success(user) = io.puts user.to_h.values.join(", ")
  18. 1 def failure(message) = logger.abort message
  19. end
  20. end
  21. end
  22. end
  23. end

lib/milestoner/cli/actions/cache/info.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Cache
  7. # Handles cache information.
  8. 1 class Info < Sod::Action
  9. 1 include Dependencies[:logger, client: :cache]
  10. 1 description "Show information."
  11. 1 on %w[-i --info]
  12. 1 def call(*)
  13. 2 path = client.path
  14. 2 then: 1 else: 1 path.exist? ? log_info("Path: #{path}.") : log_info("No cache found.")
  15. end
  16. 1 private
  17. 3 def log_info(message) = logger.info { message }
  18. end
  19. end
  20. end
  21. end
  22. end

lib/milestoner/cli/actions/cache/list.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. 1 module Cache
  7. # Handles listing users within the cache.
  8. 1 class List < Sod::Action
  9. 1 include Dependencies[:logger, :io, client: :cache]
  10. 1 description "List users."
  11. 1 on %w[-l --list]
  12. 1 def call(*)
  13. 6 logger.info { "Listing users..." }
  14. 6 client.read(:users, &:all).bind { |users| print users }
  15. end
  16. 1 private
  17. 1 def print users
  18. 5 then: 2 else: 1 return logger.info { "No users found." } if users.empty?
  19. 1 header
  20. 2 users.each { |user| io.puts user.to_h.values.map(&:inspect).join ", " }
  21. end
  22. 1 def header
  23. 1 header = "External ID, Handle, Name"
  24. 1 io.puts header
  25. 1 io.puts "-" * header.size
  26. end
  27. end
  28. end
  29. end
  30. end
  31. end

lib/milestoner/cli/actions/next.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Actions
  6. # Handles calculation of next version.
  7. 1 class Next < Sod::Action
  8. 1 include Dependencies[:settings, :io]
  9. 1 description "Show next version."
  10. 1 ancillary "Calculated from commit trailers."
  11. 1 on %w[-n --next]
  12. 1 def call(*) = io.puts settings.project_version
  13. end
  14. end
  15. end
  16. end

lib/milestoner/cli/actions/publish.rb

100.0% lines covered

100.0% branches covered

29 relevant lines. 29 lines covered and 0 lines missed.
3 total branches, 3 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "sod"
  4. 1 require "versionaire"
  5. 1 module Milestoner
  6. 1 module CLI
  7. 1 module Actions
  8. # Handles tag creation and pushing of tag to local repository.
  9. 1 class Publish < Sod::Action
  10. 1 include Dependencies[:settings, :logger]
  11. 1 include Dry::Monads[:result]
  12. 1 using Versionaire::Cast
  13. 1 description "Publish milestone."
  14. 1 ancillary "Tag and push to remote repository."
  15. 1 on %w[-p --publish], argument: "[VERSION]"
  16. 17 default { Container[:settings].project_version }
  17. 1 def initialize(publisher: Tags::Publisher.new, **)
  18. 16 super(**)
  19. 16 @publisher = publisher
  20. end
  21. 1 def call version = default
  22. 10 settings.build_max = 1
  23. 10 in: 3 case publisher.call Version(version)
  24. 3 in: 4 in Success(version) then version
  25. 4 else: 2 in Failure(message) then log_error message
  26. 2 else log_error "Publish failed, unable to parse result."
  27. end
  28. rescue Versionaire::Error => error
  29. 1 log_error error.message
  30. end
  31. 1 private
  32. 1 attr_reader :publisher
  33. 1 def log_error message
  34. 14 logger.error { message }
  35. 7 message
  36. end
  37. end
  38. end
  39. end
  40. end

lib/milestoner/cli/commands/build.rb

100.0% lines covered

100.0% branches covered

29 relevant lines. 29 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Commands
  6. # Handles the building of different milestone formats.
  7. 1 class Build < Sod::Command
  8. 1 include Dependencies[:settings, :logger, :io]
  9. 1 include Builders::Dependencies[:ascii_doc, :feed, :manifest, :markdown, :stream, :web]
  10. 1 handle "build"
  11. 1 description "Build milestone."
  12. 1 on Actions::Build::Basename
  13. 1 on Actions::Build::Format
  14. 1 on Actions::Build::Index
  15. 1 on Actions::Build::Label
  16. 1 on Actions::Build::Layout
  17. 1 on Actions::Build::Manifest
  18. 1 on Actions::Build::Max
  19. 1 on Actions::Build::Output
  20. 1 on Actions::Build::Stylesheet
  21. 1 on Actions::Build::Tail
  22. 1 on Actions::Build::Version
  23. 1 def call
  24. 12 format = settings.build_format
  25. 12 log_info "Building #{settings.project_label} (#{format})..."
  26. 12 then: 11 if infused_keys.include? format.to_sym
  27. 11 __send__(format).call
  28. else: 1 else
  29. 1 logger.abort "Invalid build format: #{format}."
  30. end
  31. 12 manifest.call
  32. end
  33. 1 private
  34. 12 def log_info(message) = logger.info { message }
  35. end
  36. end
  37. end
  38. end

lib/milestoner/cli/commands/cache.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. 1 module Commands
  6. # Handles the building of milestone output.
  7. 1 class Cache < Sod::Command
  8. 1 include Dependencies[:settings, :logger]
  9. 1 handle "cache"
  10. 1 description "Manage cache."
  11. 1 on Actions::Cache::Info
  12. 1 on Actions::Cache::List
  13. 1 on Actions::Cache::Find
  14. 1 on Actions::Cache::Create
  15. 1 on Actions::Cache::Delete
  16. end
  17. end
  18. end
  19. end

lib/milestoner/cli/shell.rb

100.0% lines covered

100.0% branches covered

24 relevant lines. 24 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "sod"
  3. 1 module Milestoner
  4. 1 module CLI
  5. # The main Command Line Interface (CLI) object.
  6. 1 class Shell
  7. 1 include Dependencies[:defaults_path, :specification, xdg_config: "xdg.config"]
  8. 1 def initialize(context: Sod::Context, dsl: Sod, **)
  9. 8 super(**)
  10. 8 @context = context
  11. 8 @dsl = dsl
  12. end
  13. 1 def call(...) = cli.call(...)
  14. 1 private
  15. 1 attr_reader :context, :dsl
  16. 1 def cli
  17. 8 context = build_context
  18. 8 dsl.new :milestoner, banner: specification.banner do
  19. 8 on(Sod::Prefabs::Commands::Config, context:)
  20. 8 on Commands::Cache
  21. 8 on Commands::Build
  22. 8 on Actions::Next
  23. 8 on Actions::Publish
  24. 8 on(Sod::Prefabs::Actions::Version, context:)
  25. 8 on Sod::Prefabs::Actions::Help, self
  26. end
  27. end
  28. 1 def build_context
  29. 8 context[defaults_path:, xdg_config:, version_label: specification.labeled_version]
  30. end
  31. end
  32. end
  33. end

lib/milestoner/commits/categorizer.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "refinements/array"
  4. 1 module Milestoner
  5. 1 module Commits
  6. # Retrieves and categorizes Git repository commit tagged or untagged history.
  7. 1 class Categorizer
  8. 1 include Dependencies[:settings]
  9. 1 using Refinements::Array
  10. 1 def initialize(collector: Collector.new, **)
  11. 37 super(**)
  12. 37 @collector = collector
  13. 37 @labels = settings.commit_categories.pluck :label
  14. 37 then: 1 else: 36 @pattern = labels.empty? ? // : Regexp.union(labels)
  15. end
  16. 1 def call min: Collector::MIN, max: Collector::MAX
  17. 331 collect(min, max).each_value { |commits| commits.sort_by! { |_, commit| commit.subject } }
  18. .values
  19. .reduce(&:concat)
  20. end
  21. 1 private
  22. 1 attr_reader :collector, :labels, :pattern
  23. 1 def collect min, max
  24. 40 collector.call(min:, max:)
  25. .value_or(Core::EMPTY_ARRAY)
  26. .each
  27. .with_index(1)
  28. .with_object categories do |(commit, position), collection|
  29. 58 category = commit.subject[pattern]
  30. 58 then: 52 else: 6 key = collection.key?(category) ? category : "Unknown"
  31. 58 collection[key] << [position, commit]
  32. end
  33. end
  34. 1 def categories
  35. 233 labels.reduce({}) { |group, prefix| group.merge prefix => [] }
  36. .merge! "Unknown" => []
  37. end
  38. end
  39. end
  40. end

lib/milestoner/commits/collector.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Commits
  5. # Collects commits since last tag, a specific range, or all commits if untagged.
  6. 1 class Collector
  7. 1 include Dry::Monads[:result]
  8. 1 include Dependencies[:git]
  9. 1 MIN = :last
  10. 1 MAX = :HEAD
  11. 64 then: 32 else: 31 def call(min: MIN, max: MAX) = git.tagged? ? slice(min, max) : all
  12. 1 private
  13. 1 def slice min, max
  14. 32 in: 10 case [min, max]
  15. 20 in: 16 in MIN, MAX then git.tag_last.bind { |tag| git.commits "#{tag}..#{max}" }
  16. 16 in: 5 in String, String then git.commits "#{min}..#{max}"
  17. 5 else: 1 in nil, String then git.commits max
  18. 1 else Failure "Invalid minimum and/or maximum range: #{min}..#{max}."
  19. end
  20. end
  21. 1 def all = git.commits
  22. end
  23. end
  24. end

lib/milestoner/commits/enricher.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Commits
  5. # Enriches commits and associated trailers for final processing.
  6. 1 class Enricher
  7. 1 include Dependencies[:settings]
  8. 1 include Enrichers::Dependencies[
  9. :author,
  10. :body_html,
  11. :collaborators,
  12. :created_at,
  13. :format,
  14. :issue,
  15. :milestone,
  16. :notes_html,
  17. :review,
  18. :signers,
  19. :updated_at,
  20. :uri
  21. ]
  22. 1 include Dry::Monads[:result]
  23. 1 def initialize(categorizer: Commits::Categorizer.new, model: Models::Commit, **)
  24. 33 super(**)
  25. 33 @categorizer = categorizer
  26. 33 @model = model
  27. end
  28. 1 def call min: Collector::MIN, max: Collector::MAX
  29. 36 categorizer.call(min:, max:)
  30. 42 .map { |(position, commit)| build_record position, commit }
  31. 36 .then { |commits| Success commits }
  32. end
  33. 1 private
  34. 1 attr_reader :categorizer, :model
  35. 1 def build_record(position, commit) = model.for commit, position:, **build_attributes(commit)
  36. 1 def build_attributes commit
  37. 42 infused_keys.each.with_object({}) do |key, attributes|
  38. 504 attributes[key] = __send__(key).call commit
  39. end
  40. end
  41. end
  42. end
  43. end

lib/milestoner/commits/enrichers/author.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Commits
  4. 1 module Enrichers
  5. # Enriches a commit author by using cache.
  6. 1 class Author
  7. 1 include Milestoner::Dependencies[:cache]
  8. 1 def initialize(model: Models::User, **)
  9. 3 super(**)
  10. 3 @model = model
  11. end
  12. 1 def call commit
  13. 142 cache.read(:users) { |table| table.find commit.author_name }
  14. .value_or(model.new)
  15. end
  16. 1 private
  17. 1 attr_reader :model
  18. end
  19. end
  20. end
  21. end

lib/milestoner/commits/enrichers/body.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Commits
  4. 1 module Enrichers
  5. # Enriches commit text by rendering as HTML based on trailer information.
  6. 1 class Body
  7. 1 include Milestoner::Dependencies[:settings]
  8. 1 def initialize(key: "Format", renderer: Renderers::Universal.new, **)
  9. 3 super(**)
  10. 3 @key = key
  11. 3 @renderer = renderer
  12. end
  13. 1 def call commit
  14. 44 commit.trailer_value_for(key)
  15. .value_or(settings.commit_format)
  16. 44 .then { |format| renderer.call commit.body, for: format.to_sym }
  17. end
  18. 1 private
  19. 1 attr_reader :key, :renderer
  20. end
  21. end
  22. end
  23. end

lib/milestoner/commits/enrichers/colleague.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "gitt"
  3. 1 module Milestoner
  4. 1 module Commits
  5. 1 module Enrichers
  6. # Enriches a commit colleague by using cache.
  7. 1 class Colleague
  8. 1 include Milestoner::Dependencies[:cache]
  9. 1 def initialize(key:, parser: Gitt::Parsers::Person.new, **)
  10. 5 super(**)
  11. 5 @key = key
  12. 5 @parser = parser
  13. end
  14. 88 def call(commit) = commit.find_trailers(key).bind { |trailers| users_for(trailers).compact }
  15. 1 private
  16. 1 attr_reader :key, :parser
  17. 5 def users_for(trailers) = trailers.map { |trailer| user_for parser.call(trailer.value) }
  18. 1 def user_for person
  19. 8 cache.read(:users) { find person.name }
  20. .value_or(nil)
  21. end
  22. end
  23. end
  24. end
  25. end

lib/milestoner/commits/enrichers/container.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "containable"
  3. 1 module Milestoner
  4. 1 module Commits
  5. 1 module Enrichers
  6. # Registers all enrichers for injection.
  7. 1 module Container
  8. 1 extend Containable
  9. 2 register(:author) { Author.new }
  10. 2 register(:body_html) { Body.new }
  11. 2 register(:collaborators) { Colleague.new key: "Co-authored-by" }
  12. 2 register(:created_at) { Time.new key: :authored_at }
  13. 2 register(:format) { Format.new }
  14. 2 register(:issue) { Issue.new }
  15. 2 register(:milestone) { Milestone.new }
  16. 2 register(:notes_html) { Note.new }
  17. 2 register(:review) { Review.new }
  18. 2 register(:signers) { Colleague.new key: "Signed-off-by" }
  19. 2 register(:updated_at) { Time.new key: :committed_at }
  20. 2 register(:uri) { URI.new }
  21. end
  22. end
  23. end
  24. end

lib/milestoner/commits/enrichers/dependencies.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "infusible"
  3. 1 module Milestoner
  4. 1 module Commits
  5. 1 module Enrichers
  6. 1 Dependencies = Infusible[Container]
  7. end
  8. end
  9. end

lib/milestoner/commits/enrichers/format.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Commits
  4. 1 module Enrichers
  5. # Enriches a commit format based on trailer information.
  6. 1 class Format
  7. 1 include Milestoner::Dependencies[:settings]
  8. 1 def initialize(key: "Format", **)
  9. 3 super(**)
  10. 3 @key = key
  11. end
  12. 1 def call(commit) = commit.trailer_value_for(key).value_or(settings.commit_format)
  13. 1 private
  14. 1 attr_reader :key
  15. end
  16. end
  17. end
  18. end

lib/milestoner/commits/enrichers/issue.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Commits
  4. 1 module Enrichers
  5. # Enriches a commit issue based on trailer information.
  6. 1 class Issue
  7. 1 include Milestoner::Dependencies[:settings]
  8. 1 def initialize(key: "Issue", model: Models::Link, **)
  9. 3 super(**)
  10. 3 @key = key
  11. 3 @model = model
  12. end
  13. 1 def call commit
  14. 44 uri = settings.tracker_uri
  15. 44 commit.trailer_value_for(key)
  16. 2 .either -> value { model[id: value, uri: format(uri, id: value)] },
  17. 42 proc { model[id: "All", uri: format(uri, id: nil)] }
  18. end
  19. 1 private
  20. 1 attr_reader :key, :model
  21. end
  22. end
  23. end
  24. end

lib/milestoner/commits/enrichers/milestone.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Commits
  4. 1 module Enrichers
  5. # Enriches a commit milestone based on trailer information.
  6. 1 class Milestone
  7. 1 include Milestoner::Dependencies[:settings]
  8. 1 def initialize(key: "Milestone", default: "unknown", **)
  9. 3 super(**)
  10. 3 @key = key
  11. 3 @default = default
  12. end
  13. 1 def call(commit) = commit.trailer_value_for(key).value_or(default)
  14. 1 private
  15. 1 attr_reader :key, :default
  16. end
  17. end
  18. end
  19. end

lib/milestoner/commits/enrichers/note.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Commits
  4. 1 module Enrichers
  5. # Enriches commit notes by rendering as HTML based on trailer information.
  6. 1 class Note
  7. 1 include Milestoner::Dependencies[:settings]
  8. 1 def initialize(key: "Format", renderer: Renderers::Universal.new, **)
  9. 3 super(**)
  10. 3 @key = key
  11. 3 @renderer = renderer
  12. end
  13. 1 def call commit
  14. 44 commit.trailer_value_for(key)
  15. .value_or(settings.commit_format)
  16. 44 .then { |format| renderer.call commit.notes, for: format.to_sym }
  17. end
  18. 1 private
  19. 1 attr_reader :key, :renderer
  20. end
  21. end
  22. end
  23. end

lib/milestoner/commits/enrichers/review.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Commits
  4. 1 module Enrichers
  5. # Enriches a commit review based on trailer information.
  6. 1 class Review
  7. 1 include Milestoner::Dependencies[:settings]
  8. 1 def initialize(model: Models::Link, **)
  9. 2 super(**)
  10. 2 @model = model
  11. end
  12. 1 def call(*) = model[id: "All", uri: format(settings.review_uri, id: nil)]
  13. 1 private
  14. 1 attr_reader :model
  15. end
  16. end
  17. end
  18. end

lib/milestoner/commits/enrichers/time.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Commits
  4. 1 module Enrichers
  5. # Enriches raw time as a Time instance.
  6. 1 class Time
  7. 1 def initialize key:
  8. 4 @key = key
  9. end
  10. 1 def call(commit) = ::Time.at commit.public_send(key).to_i
  11. 1 private
  12. 1 attr_reader :key
  13. end
  14. end
  15. end
  16. end

lib/milestoner/commits/enrichers/uri.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Commits
  4. 1 module Enrichers
  5. # Enriches a commit URI based on trailer information.
  6. 1 class URI
  7. 1 include Milestoner::Dependencies[:settings]
  8. 1 def call(commit) = format settings.commit_uri, id: commit.sha
  9. end
  10. end
  11. end
  12. end

lib/milestoner/commits/versioner.rb

100.0% lines covered

100.0% branches covered

30 relevant lines. 30 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 require "versionaire"
  5. 1 module Milestoner
  6. 1 module Commits
  7. # Calculates next version based on commit trailer version keys.
  8. 1 class Versioner
  9. 1 include Dependencies[:git, :logger]
  10. 1 include Dry::Monads[:result]
  11. 1 using Versionaire::Cast
  12. 1 DEFAULTS = {trailer_key: "Milestone", fallback: Versionaire::Version.new}.freeze
  13. 1 def initialize(defaults: DEFAULTS, collector: Collector.new, **)
  14. 434 super(**)
  15. 434 @defaults = defaults
  16. 434 @collector = collector
  17. end
  18. 1 def call
  19. 20 trailer_milestones.then { |milestones| bump milestones }
  20. .value_or(fallback)
  21. end
  22. 1 private
  23. 1 attr_reader :defaults, :collector
  24. 1 def trailer_milestones
  25. 10 collector.call.value_or(Core::EMPTY_ARRAY).each.with_object [] do |commit, values|
  26. 37 commit.trailer_value_for(trailer_key).bind { |milestone| values.append milestone.to_sym }
  27. end
  28. end
  29. 1 def bump milestones
  30. 10 last_tag_or_fallback_for milestones
  31. rescue Versionaire::Error => error
  32. 4 logger.debug { error.message }
  33. 2 Failure error
  34. end
  35. 1 def last_tag_or_fallback_for milestones
  36. 10 target = fallback.members.intersection(milestones).first
  37. 18 then: 7 else: 1 git.tag_last.fmap { |tag| target ? Version(tag).bump(target) : fallback }
  38. end
  39. 1 def trailer_key = defaults.fetch __method__
  40. 1 def fallback = defaults.fetch __method__
  41. end
  42. end
  43. end

lib/milestoner/configuration/contract.rb

100.0% lines covered

100.0% branches covered

56 relevant lines. 56 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/schema"
  3. 1 require "etcher"
  4. 1 Dry::Schema.load_extensions :monads
  5. 1 module Milestoner
  6. 1 module Configuration
  7. 1 Contract = Dry::Schema.Params do
  8. 1 required(:avatar_uri).filled :string
  9. 1 required(:build_basename).filled :string
  10. 1 required(:build_format).filled :string
  11. 1 required(:build_index).filled :bool
  12. 2 required(:build_layout) { str? | bool? }
  13. 1 required(:build_max).filled :integer
  14. 1 required(:build_manifest).filled :bool
  15. 1 required(:build_output).filled Etcher::Types::Pathname
  16. 1 required(:build_stylesheet).filled :bool
  17. 1 required(:build_tail).filled :string
  18. 1 required(:build_template_paths).array Etcher::Types::Pathname
  19. 1 required(:commit_categories).array(:hash) do
  20. 1 required(:emoji).filled :string
  21. 1 required(:label).filled :string
  22. end
  23. 1 required(:commit_format).filled :string
  24. 1 required(:commit_uri).filled :string
  25. 1 required(:generator_label).filled :string
  26. 1 required(:generator_uri).filled :string
  27. 1 required(:generator_version).filled Etcher::Types::Version
  28. 1 required(:loaded_at).filled :time
  29. 1 required(:organization_label).filled :string
  30. 1 required(:organization_uri).filled :string
  31. 1 required(:profile_uri).filled :string
  32. 1 required(:project_author).filled :string
  33. 1 optional(:project_description).filled :string
  34. 1 optional(:project_label).filled :string
  35. 1 required(:project_name).filled :string
  36. 1 required(:project_owner).filled :string
  37. 1 required(:project_uri_home).filled :string
  38. 1 optional(:project_uri_icon).filled :string
  39. 1 optional(:project_uri_logo).filled :string
  40. 1 required(:project_uri_version).filled :string
  41. 1 required(:project_version).filled Etcher::Types::Version
  42. 1 required(:review_uri).filled :string
  43. 1 required(:stylesheet_path).filled :string
  44. 1 required(:stylesheet_uri).filled :string
  45. 1 required(:syndication_categories).array(:hash) do
  46. 1 required(:label).filled :string
  47. 1 required(:name).filled :string
  48. end
  49. 1 required(:syndication_entry_label).filled :string
  50. 1 required(:syndication_entry_uri).filled :string
  51. 1 required(:syndication_id).filled :string
  52. 1 required(:syndication_label).filled :string
  53. 1 required(:syndication_links).array(:hash) do
  54. 1 required(:label).filled :string
  55. 1 required(:mime).filled :string
  56. 1 required(:relation).filled :string
  57. 1 required(:uri).filled :string
  58. end
  59. 1 required(:tag_subject).filled :string
  60. 1 required(:tracker_uri).filled :string
  61. end
  62. end
  63. end

lib/milestoner/configuration/model.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Configuration
  4. # Defines configuration content as the primary source of truth for use throughout the gem.
  5. 1 Model = Struct.new :avatar_uri,
  6. :build_basename,
  7. :build_format,
  8. :build_index,
  9. :build_layout,
  10. :build_manifest,
  11. :build_max,
  12. :build_output,
  13. :build_stylesheet,
  14. :build_tail,
  15. :build_template_paths,
  16. :commit_categories,
  17. :commit_format,
  18. :commit_uri,
  19. :generator_label,
  20. :generator_uri,
  21. :generator_version,
  22. :loaded_at,
  23. :organization_label,
  24. :organization_uri,
  25. :profile_uri,
  26. :project_author,
  27. :project_description,
  28. :project_label,
  29. :project_name,
  30. :project_owner,
  31. :project_uri_home,
  32. :project_uri_icon,
  33. :project_uri_logo,
  34. :project_uri_version,
  35. :project_version,
  36. :review_uri,
  37. :stylesheet_path,
  38. :stylesheet_uri,
  39. :syndication_categories,
  40. :syndication_entry_label,
  41. :syndication_entry_uri,
  42. :syndication_id,
  43. :syndication_label,
  44. :syndication_links,
  45. :tag_subject,
  46. :tracker_uri
  47. end
  48. end

lib/milestoner/configuration/transformers/build/template_paths.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pathname"
  4. 1 require "runcom"
  5. 1 module Milestoner
  6. 1 module Configuration
  7. 1 module Transformers
  8. 1 module Build
  9. # Ensures XDG configuration and gem template paths are configured.
  10. 1 class TemplatePaths
  11. 1 include Dry::Monads[:result]
  12. 1 def initialize key = :build_template_paths,
  13. default: Pathname(__dir__).join("../../../templates"),
  14. xdg: Runcom::Config.new("milestoner/templates")
  15. 425 @key = key
  16. 425 @default = default
  17. 425 @xdg = xdg
  18. end
  19. 1 def call(attributes) = Success attributes.merge!(key => xdg.all.append(default))
  20. 1 private
  21. 1 attr_reader :key, :default, :xdg
  22. end
  23. end
  24. end
  25. end
  26. end

lib/milestoner/configuration/transformers/citations/description.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "cff"
  3. 1 require "dry/monads"
  4. 1 require "pathname"
  5. 1 module Milestoner
  6. 1 module Configuration
  7. 1 module Transformers
  8. 1 module Citations
  9. # Conditionally updates project description based on citation details.
  10. 1 class Description
  11. 1 include Dry::Monads[:result]
  12. 1 def initialize key = :project_description,
  13. path: Pathname.pwd.join("CITATION.cff"),
  14. citation: CFF::File
  15. 427 @key = key
  16. 427 @path = path
  17. 427 @citation = citation
  18. end
  19. 1 def call attributes
  20. 427 attributes.fetch key do
  21. 426 value = citation.open(path).abstract
  22. 426 else: 1 then: 425 attributes.merge! key => value unless value.empty?
  23. end
  24. 427 Success attributes
  25. end
  26. 1 private
  27. 1 attr_reader :key, :path, :citation
  28. end
  29. end
  30. end
  31. end
  32. end

lib/milestoner/configuration/transformers/citations/label.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "cff"
  3. 1 require "dry/monads"
  4. 1 require "pathname"
  5. 1 module Milestoner
  6. 1 module Configuration
  7. 1 module Transformers
  8. 1 module Citations
  9. # Conditionally updates project label based on citation details.
  10. 1 class Label
  11. 1 include Dry::Monads[:result]
  12. 1 def initialize key = :project_label,
  13. path: Pathname.pwd.join("CITATION.cff"),
  14. citation: CFF::File
  15. 427 @key = key
  16. 427 @path = path
  17. 427 @citation = citation
  18. end
  19. 1 def call attributes
  20. 427 attributes.fetch key do
  21. 426 value = citation.open(path).title
  22. 426 else: 1 then: 425 attributes.merge! key => value unless value.empty?
  23. end
  24. 427 Success attributes
  25. end
  26. 1 private
  27. 1 attr_reader :key, :path, :citation
  28. end
  29. end
  30. end
  31. end
  32. end

lib/milestoner/configuration/transformers/citations/uri.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "cff"
  3. 1 require "dry/monads"
  4. 1 require "pathname"
  5. 1 module Milestoner
  6. 1 module Configuration
  7. 1 module Transformers
  8. 1 module Citations
  9. # Conditionally updates project URI based on citation URL.
  10. 1 class URI
  11. 1 include Dry::Monads[:result]
  12. 1 def initialize key = :project_uri,
  13. path: Pathname.pwd.join("CITATION.cff"),
  14. citation: CFF::File
  15. 429 @key = key
  16. 429 @path = path
  17. 429 @citation = citation
  18. end
  19. 1 def call attributes
  20. 429 process attributes
  21. 428 Success attributes
  22. rescue KeyError => error
  23. 1 Failure step: :transform,
  24. payload: "Unable to transform #{key.inspect}, missing specifier: " \
  25. "\"#{error.message[/<.+>/]}\"."
  26. end
  27. 1 private
  28. 1 attr_reader :key, :path, :citation
  29. 1 def process attributes
  30. 855 attributes.fetch(key) { citation.open(path).url }
  31. 429 then: 2 else: 427 .then { |value| value.match?(/%<.+>s/) ? format(value, attributes) : value }
  32. 428 else: 1 then: 427 .then { |value| attributes.merge! key => value unless value.empty? }
  33. end
  34. end
  35. end
  36. end
  37. end
  38. end

lib/milestoner/configuration/transformers/gems/description.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pathname"
  4. 1 module Milestoner
  5. 1 module Configuration
  6. 1 module Transformers
  7. 1 module Gems
  8. # Conditionally updates project description based on specification summary.
  9. 1 class Description
  10. 1 include Dependencies[:spec_loader]
  11. 1 include Dry::Monads[:result]
  12. 1 def initialize(key = :project_description, path: "#{Pathname.pwd.basename}.gemspec", **)
  13. 427 super(**)
  14. 427 @key = key
  15. 427 @path = path
  16. end
  17. 1 def call attributes
  18. 427 attributes.fetch key do
  19. 426 value = spec_loader.call(path).summary
  20. 426 then: 1 else: 425 attributes.merge! key => value if value
  21. end
  22. 427 Success attributes
  23. end
  24. 1 private
  25. 1 attr_reader :key, :path
  26. end
  27. end
  28. end
  29. end
  30. end

lib/milestoner/configuration/transformers/gems/label.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pathname"
  4. 1 module Milestoner
  5. 1 module Configuration
  6. 1 module Transformers
  7. 1 module Gems
  8. # Conditionally updates project label based on specification label.
  9. 1 class Label
  10. 1 include Dependencies[:spec_loader]
  11. 1 include Dry::Monads[:result]
  12. 1 def initialize(key = :project_label, path: "#{Pathname.pwd.basename}.gemspec", **)
  13. 427 super(**)
  14. 427 @key = key
  15. 427 @path = path
  16. end
  17. 1 def call attributes
  18. 427 attributes.fetch key do
  19. 426 value = spec_loader.call(path).label
  20. 426 else: 425 then: 1 attributes.merge! key => value unless value.empty?
  21. end
  22. 427 Success attributes
  23. end
  24. 1 private
  25. 1 attr_reader :key, :path
  26. end
  27. end
  28. end
  29. end
  30. end

lib/milestoner/configuration/transformers/gems/name.rb

100.0% lines covered

100.0% branches covered

20 relevant lines. 20 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pathname"
  4. 1 module Milestoner
  5. 1 module Configuration
  6. 1 module Transformers
  7. 1 module Gems
  8. # Conditionally updates project name based on specification name.
  9. 1 class Name
  10. 1 include Dependencies[:spec_loader]
  11. 1 include Dry::Monads[:result]
  12. 1 def initialize(key = :project_name, path: "#{Pathname.pwd.basename}.gemspec", **)
  13. 427 super(**)
  14. 427 @key = key
  15. 427 @path = path
  16. end
  17. 1 def call attributes
  18. 427 attributes.fetch key do
  19. 3 value = spec_loader.call(path).name
  20. 3 then: 1 else: 2 attributes.merge! key => value if value
  21. end
  22. 427 Success attributes
  23. end
  24. 1 private
  25. 1 attr_reader :key, :path
  26. end
  27. end
  28. end
  29. end
  30. end

lib/milestoner/configuration/transformers/gems/uri.rb

100.0% lines covered

100.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pathname"
  4. 1 module Milestoner
  5. 1 module Configuration
  6. 1 module Transformers
  7. 1 module Gems
  8. # Conditionally updates project URI based on specification home page URL.
  9. 1 class URI
  10. 1 include Dependencies[:spec_loader]
  11. 1 include Dry::Monads[:result]
  12. 1 def initialize(key = :project_uri, path: "#{Pathname.pwd.basename}.gemspec", **)
  13. 429 super(**)
  14. 429 @key = key
  15. 429 @path = path
  16. end
  17. 1 def call attributes
  18. 429 process attributes
  19. 428 Success attributes
  20. rescue KeyError => error
  21. 1 Failure step: :transform,
  22. payload: "Unable to transform #{key.inspect}, missing specifier: " \
  23. "\"#{error.message[/<.+>/]}\"."
  24. end
  25. 1 private
  26. 1 attr_reader :key, :path
  27. 1 def process attributes
  28. 855 attributes.fetch(key) { spec_loader.call(path).homepage_url }
  29. 429 then: 2 else: 427 .then { |value| value.match?(/%<.+>s/) ? format(value, attributes) : value }
  30. 428 else: 425 then: 3 .then { |value| attributes.merge! key => value unless value.empty? }
  31. end
  32. end
  33. end
  34. end
  35. end
  36. end

lib/milestoner/configuration/transformers/generator/label.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Configuration
  5. 1 module Transformers
  6. 1 module Generator
  7. # Conditionally updates generator label based on gem specification.
  8. 1 class Label
  9. 1 include Dependencies[:specification]
  10. 1 include Dry::Monads[:result]
  11. 1 def initialize(key = :generator_label, **)
  12. 426 super(**)
  13. 426 @key = key
  14. end
  15. 1 def call attributes
  16. 851 attributes.fetch(key) { attributes.merge! key => specification.label }
  17. 426 Success attributes
  18. end
  19. 1 private
  20. 1 attr_reader :key
  21. end
  22. end
  23. end
  24. end
  25. end

lib/milestoner/configuration/transformers/generator/uri.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Configuration
  5. 1 module Transformers
  6. 1 module Generator
  7. # Conditionally updates generator URI based on gem specification.
  8. 1 class URI
  9. 1 include Dependencies[:specification]
  10. 1 include Dry::Monads[:result]
  11. 1 def initialize(key = :generator_uri, **)
  12. 426 super(**)
  13. 426 @key = key
  14. end
  15. 1 def call attributes
  16. 851 attributes.fetch(key) { attributes.merge! key => specification.homepage_url }
  17. 426 Success attributes
  18. end
  19. 1 private
  20. 1 attr_reader :key
  21. end
  22. end
  23. end
  24. end
  25. end

lib/milestoner/configuration/transformers/generator/version.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Configuration
  5. 1 module Transformers
  6. 1 module Generator
  7. # Conditionally updates generator version based on gem specification.
  8. 1 class Version
  9. 1 include Dependencies[:specification]
  10. 1 include Dry::Monads[:result]
  11. 1 def initialize(key = :generator_version, **)
  12. 426 super(**)
  13. 426 @key = key
  14. end
  15. 1 def call attributes
  16. 851 attributes.fetch(key) { attributes.merge! key => specification.version }
  17. 426 Success attributes
  18. end
  19. 1 private
  20. 1 attr_reader :key
  21. end
  22. end
  23. end
  24. end
  25. end

lib/milestoner/configuration/transformers/project/author.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Configuration
  5. 1 module Transformers
  6. 1 module Project
  7. # Conditionally updates author based on Git user.
  8. 1 class Author
  9. 1 include Dependencies[:git]
  10. 1 include Dry::Monads[:result]
  11. 1 def initialize(key = :project_author, **)
  12. 428 super(**)
  13. 428 @key = key
  14. end
  15. 1 def call attributes
  16. 428 attributes.fetch key do
  17. 853 then: 425 else: 1 git.get("user.name", nil).bind { |value| attributes.merge! key => value if value }
  18. end
  19. 428 Success attributes
  20. end
  21. 1 private
  22. 1 attr_reader :key
  23. end
  24. end
  25. end
  26. end
  27. end

lib/milestoner/configuration/transformers/project/label.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "pathname"
  4. 1 require "refinements/string"
  5. 1 module Milestoner
  6. 1 module Configuration
  7. 1 module Transformers
  8. # Conditionally updates label based on current directory.
  9. 1 module Project
  10. 1 using Refinements::String
  11. 1 Label = lambda do |attributes, key = :project_label, default: Pathname.pwd.basename.to_s|
  12. 429 attributes.fetch(key) { attributes.merge! key => default.titleize }
  13. 427 Dry::Monads::Success attributes
  14. end
  15. end
  16. end
  17. end
  18. end

lib/milestoner/configuration/transformers/project/version.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/hash"
  4. 1 module Milestoner
  5. 1 module Configuration
  6. 1 module Transformers
  7. 1 module Project
  8. # Conditionally updates version based on last Git tag.
  9. 1 class Version
  10. 1 include Dry::Monads[:result]
  11. 1 using Refinements::Hash
  12. 1 def initialize key = :project_version, versioner: Commits::Versioner.new
  13. 426 @key = key
  14. 426 @versioner = versioner
  15. end
  16. 1 def call attributes
  17. 428 attributes.fetch_value(key) { attributes.merge! key => versioner.call }
  18. 426 Success attributes
  19. end
  20. 1 private
  21. 1 attr_reader :key, :versioner
  22. end
  23. end
  24. end
  25. end
  26. end

lib/milestoner/configuration/transformers/syndication/link.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 require "refinements/hash"
  5. 1 module Milestoner
  6. 1 module Configuration
  7. 1 module Transformers
  8. # Conditionally updates links based on project details.
  9. 1 module Syndication
  10. 1 using Refinements::Hash
  11. 1 Link = lambda do |attributes, key = :syndication_links|
  12. 429 links = attributes.fetch key, Core::EMPTY_ARRAY
  13. 429 links.each do |link|
  14. 854 link.symbolize_keys!
  15. 854 link[:label] = format link[:label], attributes
  16. 853 link[:uri] = format link[:uri], attributes
  17. end
  18. 427 Dry::Monads::Success attributes.merge!(key => links)
  19. rescue KeyError => error
  20. 2 Dry::Monads::Failure step: :transform,
  21. payload: "Unable to transform #{key.inspect}, missing specifier: " \
  22. "\"#{error.message[/<.+>/]}\"."
  23. end
  24. end
  25. end
  26. end
  27. end

lib/milestoner/container.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "cogger"
  3. 1 require "containable"
  4. 1 require "etcher"
  5. 1 require "gitt"
  6. 1 require "lode"
  7. 1 require "runcom"
  8. 1 require "spek"
  9. 1 require "tone"
  10. 1 module Milestoner
  11. # Provides a global gem container for injection into other objects.
  12. 1 module Container
  13. 1 extend Containable
  14. 1 namespace :xdg do
  15. 1 register(:cache) { Runcom::Cache.new "milestoner/database.store" }
  16. 2 register(:config) { Runcom::Config.new "milestoner/configuration.yml" }
  17. end
  18. 1 register :cache do
  19. skipped # :nocov:
  20. skipped Lode.new self["xdg.cache"].passive do |config|
  21. skipped config.mode = :thread
  22. skipped config.table = Lode::Tables::Value
  23. skipped config.register :users, model: Models::User, primary_key: :name
  24. skipped end
  25. skipped end
  26. skipped
  27. skipped register :registry, as: :fresh do
  28. skipped Etcher::Registry.new(contract: Configuration::Contract, model: Configuration::Model)
  29. skipped .add_loader(:yaml, self[:defaults_path])
  30. skipped .add_loader(:yaml, self["xdg.config"].active)
  31. skipped .add_transformer(:root, :build_output)
  32. skipped .add_transformer(Configuration::Transformers::Build::TemplatePaths.new)
  33. skipped .add_transformer(Configuration::Transformers::Gems::Label.new)
  34. skipped .add_transformer(Configuration::Transformers::Gems::Description.new)
  35. skipped .add_transformer(Configuration::Transformers::Gems::Name.new)
  36. skipped .add_transformer(Configuration::Transformers::Gems::URI.new)
  37. skipped .add_transformer(Configuration::Transformers::Citations::Label.new)
  38. skipped .add_transformer(Configuration::Transformers::Citations::Description.new)
  39. skipped .add_transformer(Configuration::Transformers::Citations::URI.new)
  40. skipped .add_transformer(Configuration::Transformers::Project::Author.new)
  41. skipped .add_transformer(Configuration::Transformers::Project::Label)
  42. skipped .add_transformer(:basename, :project_name)
  43. skipped .add_transformer(Configuration::Transformers::Project::Version.new)
  44. skipped .add_transformer(:format, :project_uri_home)
  45. skipped .add_transformer(:format, :project_uri_icon)
  46. skipped .add_transformer(:format, :project_uri_logo)
  47. skipped .add_transformer(:format, :project_uri_version, id: "%<id>s")
  48. skipped .add_transformer(Configuration::Transformers::Generator::Label.new)
  49. skipped .add_transformer(Configuration::Transformers::Generator::URI.new)
  50. skipped .add_transformer(Configuration::Transformers::Generator::Version.new)
  51. skipped .add_transformer(:format, :syndication_entry_label, id: "%<id>s")
  52. skipped .add_transformer(:format, :syndication_entry_uri, id: "%<id>s")
  53. skipped .add_transformer(:format, :syndication_id, id: "%<id>s")
  54. skipped .add_transformer(:format, :syndication_label)
  55. skipped .add_transformer(Configuration::Transformers::Syndication::Link)
  56. skipped .add_transformer(:format, :commit_uri, id: "%<id>s")
  57. skipped .add_transformer(:format, :review_uri, id: "%<id>s")
  58. skipped .add_transformer(:format, :tracker_uri, id: "%<id>s")
  59. skipped .add_transformer(:time, :loaded_at)
  60. skipped end
  61. skipped
  62. skipped register(:settings) { Etcher.call(self[:registry]).dup }
  63. skipped register(:specification) { self[:spec_loader].call "#{__dir__}/../../milestoner.gemspec" }
  64. skipped register(:sanitizer) { Sanitizer.new }
  65. skipped register(:spec_loader) { Spek::Loader.new }
  66. skipped register(:git) { Gitt::Repository.new }
  67. skipped register(:defaults_path) { Pathname(__dir__).join("configuration/defaults.yml") }
  68. skipped register(:color) { Tone.new }
  69. skipped register :durationer, Durationer
  70. skipped register(:logger) { Cogger.new id: :milestoner }
  71. skipped register :io, STDOUT
  72. skipped end
  73. skipped end

lib/milestoner/dependencies.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "infusible"
  3. 1 module Milestoner
  4. 1 Dependencies = Infusible[Container]
  5. end

lib/milestoner/durationer.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/array"
  3. 1 require "refinements/string"
  4. # Computes duration (in seconds) into human readable years, days, hours, minutes, and seconds.
  5. 1 module Milestoner
  6. 1 using Refinements::Array
  7. 1 using Refinements::String
  8. 1 DURATION_UNITS = {
  9. "year" => 31_536_000, # 60 * 60 * 25 * 365
  10. "day" => 86_400, # 60 * 60 * 25
  11. "hour" => 3_600, # 60 * 60
  12. "minute" => 60,
  13. "second" => 1
  14. }.freeze
  15. 1 Durationer = lambda do |seconds, units: DURATION_UNITS|
  16. 161 then: 2 else: 159 return "0 seconds" if seconds.negative? || seconds.zero?
  17. 159 result = units.map do |unit, divisor|
  18. 795 count, seconds = seconds.divmod divisor
  19. 795 then: 328 else: 467 next if count.zero?
  20. 467 %(#{count} #{unit.pluralize "s", count})
  21. end
  22. 159 result.compact.to_sentence
  23. end
  24. end

lib/milestoner/models/commit.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "gitt"
  3. 1 module Milestoner
  4. 1 module Models
  5. 1 COMMIT_COMMON_ATTRIBUTES = %i[
  6. authored_at
  7. authored_relative_at
  8. body
  9. committed_at
  10. committed_relative_at
  11. deletions
  12. encoding
  13. files_changed
  14. fingerprint
  15. fingerprint_key
  16. insertions
  17. notes
  18. sha
  19. signature
  20. subject
  21. ].freeze
  22. 1 COMMIT_ENRICHED_ATTRIBUTES = %i[
  23. author
  24. body_html
  25. collaborators
  26. created_at
  27. format
  28. issue
  29. milestone
  30. notes_html
  31. position
  32. review
  33. signers
  34. updated_at
  35. uri
  36. ].freeze
  37. # Represents an enriched commit.
  38. 1 Commit = Struct.new(*COMMIT_COMMON_ATTRIBUTES, *COMMIT_ENRICHED_ATTRIBUTES) do
  39. 1 include Gitt::Directable
  40. 1 def self.for(commit, **) = new(**commit.to_h.slice(*COMMIT_COMMON_ATTRIBUTES), **)
  41. 1 def initialize(**)
  42. 268 super
  43. 268 freeze
  44. end
  45. 38 def contributors = [author, *collaborators, *signers].tap(&:uniq!).sort_by! { it.name.to_s }
  46. end
  47. end
  48. end

lib/milestoner/models/link.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Models
  4. # Represents a hyperlink.
  5. 1 Link = Data.define :id, :uri do
  6. 1 def initialize id: nil, uri: nil
  7. 490 super
  8. end
  9. end
  10. end
  11. end

lib/milestoner/models/tag.rb

100.0% lines covered

100.0% branches covered

3 relevant lines. 3 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Models
  4. # Represents a Git tag comprised of multiple commits.
  5. 1 Tag = Struct.new :author, :commits, :committed_at, :contributors, :sha, :signature, :version
  6. end
  7. end

lib/milestoner/models/user.rb

100.0% lines covered

100.0% branches covered

5 relevant lines. 5 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Models
  4. # Represents an external user.
  5. 1 User = Data.define :external_id, :handle, :name do
  6. 1 def initialize external_id: nil, handle: nil, name: nil
  7. 1226 super
  8. end
  9. end
  10. end
  11. end

lib/milestoner/renderers/asciidoc.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "asciidoctor"
  3. 1 module Milestoner
  4. 1 module Renderers
  5. # Renders ASCII Doc as HTML.
  6. 1 class Asciidoc
  7. 1 SETTINGS = {
  8. safe: :safe,
  9. attributes: {
  10. "source-highlighter" => "rouge",
  11. "rouge-linenums-mode" => "inline"
  12. }
  13. }.freeze
  14. 1 def initialize settings: SETTINGS, client: Asciidoctor
  15. 3 @settings = settings
  16. 3 @client = client
  17. end
  18. 1 def call(content) = client.convert content, settings
  19. 1 private
  20. 1 attr_reader :settings, :client
  21. end
  22. end
  23. end

lib/milestoner/renderers/markdown.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "redcarpet"
  3. 1 require "refinements/module"
  4. 1 require "rouge"
  5. 1 require "rouge/plugins/redcarpet"
  6. 1 module Milestoner
  7. 1 module Renderers
  8. # Renders Markdown as HTML.
  9. 1 class Markdown
  10. 1 using Refinements::Module
  11. 1 CLIENT = Redcarpet::Markdown.new Class.new(Redcarpet::Render::HTML)
  12. .include(Rouge::Plugins::Redcarpet)
  13. .pseudonym("redcarpet_html_rouge")
  14. .new,
  15. disable_indented_code_blocks: true,
  16. fenced_code_blocks: true,
  17. footnotes: true,
  18. highlight: true,
  19. superscript: true,
  20. tables: true
  21. 1 def initialize client: CLIENT
  22. 7 @client = client
  23. end
  24. 1 def call(content) = client.render content
  25. 1 private
  26. 1 attr_reader :client
  27. end
  28. end
  29. end

lib/milestoner/renderers/universal.rb

100.0% lines covered

100.0% branches covered

14 relevant lines. 14 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/binding"
  3. 1 module Milestoner
  4. 1 module Renderers
  5. # The primary renderer for multiple input formats as HTML.
  6. 1 class Universal
  7. 1 include Dependencies[:settings]
  8. 1 using Refinements::Binding
  9. 1 DELEGATES = {asciidoc: Asciidoc.new, markdown: Markdown.new}.freeze
  10. 1 def initialize(delegates: DELEGATES, **)
  11. 9 super(**)
  12. 9 @delegates = delegates
  13. 9 @default_format = settings.commit_format.to_sym
  14. end
  15. 1 def call(content, for: default_format) = delegates.fetch(binding[:for]).call content
  16. 1 private
  17. 1 attr_reader :delegates, :default_format
  18. end
  19. end
  20. end

lib/milestoner/sanitizer.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "refinements/array"
  3. 1 require "sanitize"
  4. 1 module Milestoner
  5. # A custom HTML sanitizer.
  6. 1 class Sanitizer
  7. 1 using Refinements::Array
  8. 1 def initialize defaults: Sanitize::Config::BASIC, client: Sanitize
  9. 171 @defaults = defaults
  10. 171 @client = client
  11. end
  12. 1 def call(content) = client.fragment content, configuration
  13. 1 private
  14. 1 attr_reader :defaults, :client
  15. 1 def configuration = client::Config.merge(defaults, elements:, attributes:)
  16. 1 def elements
  17. 214 defaults[:elements].including "audio", "details", "img", "source", "span", "summary", "video"
  18. end
  19. 1 def attributes
  20. 214 defaults[:attributes].merge(
  21. all: %w[class id],
  22. "a" => %w[href title],
  23. "audio" => %w[autoplay controls controlslist crossorigin loop muted preload src],
  24. "details" => %w[name open],
  25. "img" => %w[alt height loading src width],
  26. "source" => %w[type src srcset sizes media height width],
  27. "video" => %w[controls height poster src width]
  28. )
  29. end
  30. end
  31. end

lib/milestoner/tags/builder.rb

100.0% lines covered

100.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 module Milestoner
  3. 1 module Tags
  4. # Builds tag message.
  5. 1 class Builder
  6. 1 include Milestoner::Dependencies[:settings, :logger]
  7. 1 def initialize(enricher: Enricher.new, view: Views::Milestones::Show.new, **)
  8. 19 super(**)
  9. 19 @enricher = enricher
  10. 19 @view = view
  11. end
  12. 1 def call version
  13. 10 force_minimum
  14. 10 enricher.call
  15. 8 .fmap { |tags| render tags.first, version }
  16. 2 .alt_map { |message| failure message }
  17. end
  18. 1 private
  19. 1 attr_reader :enricher, :view
  20. 1 def force_minimum = settings.build_max = 1
  21. 1 def render tag, version
  22. 8 tag.version = version
  23. 8 view.call(tag:, layout: settings.build_layout, format: :git).to_s.lstrip
  24. end
  25. 1 def failure message
  26. 4 logger.error { message }
  27. 2 message
  28. end
  29. end
  30. end
  31. end

lib/milestoner/tags/creator.rb

100.0% lines covered

100.0% branches covered

25 relevant lines. 25 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "dry/monads"
  4. 1 module Milestoner
  5. 1 module Tags
  6. # Handles the creation of project repository tags.
  7. 1 class Creator
  8. 1 include Dependencies[:settings, :git, :logger]
  9. 1 include Dry::Monads[:result]
  10. 1 def initialize(collector: Commits::Collector.new, builder: Builder.new, **)
  11. 15 super(**)
  12. 15 @collector = collector
  13. 15 @builder = builder
  14. end
  15. 1 def call version
  16. 9 then: 2 else: 7 return Success version if local? version
  17. 13 collect.bind { create version }
  18. end
  19. 1 private
  20. 1 attr_reader :collector, :builder
  21. 1 def local? version
  22. 9 then: 2 if git.tag_local? version
  23. 4 logger.warn { "Local tag exists: #{version}. Skipped." }
  24. 2 true
  25. else: 7 else
  26. 7 false
  27. end
  28. end
  29. 2 def collect = collector.call.alt_map { |message| message.sub("fatal: y", "Y").sub("\n", ".") }
  30. 1 def create version
  31. 12 builder.call(version).bind { |body| git.tag_create version, "#{subject}\n\n#{body}\n\n" }
  32. end
  33. 1 def subject = format(settings.tag_subject, **settings.to_h)
  34. end
  35. end
  36. end

lib/milestoner/tags/enricher.rb

100.0% lines covered

100.0% branches covered

57 relevant lines. 57 lines covered and 0 lines missed.
12 total branches, 12 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 require "refinements/pathname"
  4. 1 require "refinements/struct"
  5. 1 require "versionaire"
  6. 1 module Milestoner
  7. 1 module Tags
  8. # Builds a collection of enriched tags and associated commits.
  9. # :reek:TooManyMethods
  10. 1 class Enricher
  11. 1 include Milestoner::Dependencies[:git, :settings, :logger]
  12. 1 include Commits::Enrichers::Dependencies[author_enricher: :author]
  13. 1 include Dry::Monads[:result]
  14. 1 using Refinements::Pathname
  15. 1 using Versionaire::Cast
  16. 1 using Refinements::Struct
  17. 1 def initialize(committer: Commits::Enricher.new, model: Models::Tag, **)
  18. 32 super(**)
  19. 32 @committer = committer
  20. 32 @model = model
  21. end
  22. 1 def call
  23. 48 collect.fmap { |tags| adjust tags }
  24. 24 .fmap { |references| slice(references).reverse }
  25. 24 then: 2 else: 22 .bind { |tags| tags.empty? ? Failure("No tags or commits.") : Success(tags) }
  26. end
  27. 1 private
  28. 1 attr_reader :committer, :model
  29. 25 then: 16 else: 8 def collect = git.tagged? ? git.tags("--sort=taggerdate") : placeholder_with_commits
  30. 1 def adjust tags
  31. 24 references = tags.last(settings.build_max).map(&:version)
  32. 24 maybe_append_head references
  33. 24 maybe_prepend_nil references, tags
  34. 24 references
  35. end
  36. 1 def maybe_append_head references
  37. 24 then: 20 else: 4 references.append "HEAD" if settings.build_tail == "head"
  38. end
  39. 1 def maybe_prepend_nil references, tags
  40. 24 max = settings.build_max
  41. 24 tail = settings.build_tail
  42. 24 then: 5 else: 19 references.prepend nil if references.one? || (tail == "tag" && max >= tags.size)
  43. end
  44. 1 def slice references
  45. 24 references.each_cons(2).with_object [] do |(min, max), entries|
  46. 27 add_enrichment entries, min, max
  47. end
  48. end
  49. 26 def add_enrichment(all, *) = enrich(*).bind { |entry| all.append entry }
  50. 1 def enrich min, max
  51. 54 committer.call(min:, max:).bind { |commits| build_record git.tag_show(max), commits }
  52. end
  53. 1 def build_record result, commits
  54. 54 result.fmap { |tag| record_for tag, commits }
  55. rescue Versionaire::Error => error
  56. 2 logger.error error.message
  57. 2 Failure error
  58. end
  59. 1 def record_for tag, commits
  60. 27 model[
  61. author: author(tag),
  62. commits:,
  63. committed_at: committed_at(tag.committed_at),
  64. contributors: contributors(commits),
  65. sha: tag.sha,
  66. signature: tag.signature,
  67. version: Version(tag.version || settings.project_version)
  68. ]
  69. end
  70. 9 def placeholder_with_commits = committer.call.fmap { |commits| placeholder_for commits }
  71. 1 def placeholder_for commits
  72. 8 then: 1 else: 7 return commits if commits.empty?
  73. [
  74. 7 model[
  75. author: commits.last.author,
  76. commits:,
  77. committed_at: Time.now,
  78. contributors: [],
  79. version: settings.project_version
  80. ]
  81. ]
  82. end
  83. 1 def author tag
  84. 27 author_enricher.call tag.with(author_name: tag.author_name || settings.project_author)
  85. end
  86. # :reek:UtilityFunction
  87. 1 def contributors commits
  88. 58 commits.reduce([]) { |all, commit| all.append(*commit.contributors) }
  89. .uniq
  90. 23 .sort_by { it.name.to_s }
  91. end
  92. 28 then: 7 else: 20 def committed_at(at) = at ? Time.at(at.to_i) : settings.loaded_at
  93. end
  94. end
  95. end

lib/milestoner/tags/manifest.rb

100.0% lines covered

100.0% branches covered

27 relevant lines. 27 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "json"
  4. 1 require "refinements/hash"
  5. 1 require "refinements/pathname"
  6. 1 module Milestoner
  7. 1 module Tags
  8. # Manages build manifest.
  9. 1 class Manifest
  10. 1 include Dependencies[:settings, :git]
  11. 1 using Refinements::Hash
  12. 1 using Refinements::Pathname
  13. 1 def initialize(name: "manifest.json", **)
  14. 20 super(**)
  15. 20 @name = name
  16. end
  17. 1 def build_path = settings.build_output.join name
  18. 1 def diff path = build_path
  19. 3 git.tags.value_or(Core::EMPTY_ARRAY).then do |tags|
  20. 3 then: 1 else: 2 return Core::EMPTY_HASH if tags.empty?
  21. 2 content_for(tags).diff read(path)
  22. end
  23. end
  24. 1 def read(path = build_path) = JSON(path.read, {symbolize_names: true})
  25. 1 def write(path = build_path, **)
  26. 11 path.make_ancestors.write JSON.pretty_generate(generator.deep_merge(**))
  27. end
  28. 1 private
  29. 1 attr_reader :name
  30. 1 def content_for tags
  31. 2 generator.merge latest: tags.last.version, versions: tags.map(&:version)
  32. end
  33. 1 def generator
  34. 13 {generator: {label: settings.generator_label, version: settings.generator_version.to_s}}
  35. end
  36. end
  37. end
  38. end

lib/milestoner/tags/publisher.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Tags
  5. # Handles the tagging and pushing of a tag to a remote repository.
  6. 1 class Publisher
  7. 1 include Dependencies[:logger]
  8. 1 include Dry::Monads[:result]
  9. 1 def initialize(creator: Tags::Creator.new, pusher: Tags::Pusher.new, **)
  10. 14 super(**)
  11. 14 @creator = creator
  12. 14 @pusher = pusher
  13. end
  14. 1 def call version
  15. 8 creator.call(version)
  16. 6 .bind { pusher.call version }
  17. 4 .bind { log_info version }
  18. end
  19. 1 private
  20. 1 attr_reader :creator, :pusher
  21. 1 def log_info version
  22. 8 logger.info { "Published: #{version}" }
  23. 4 Success version
  24. end
  25. end
  26. end
  27. end

lib/milestoner/tags/pusher.rb

100.0% lines covered

100.0% branches covered

21 relevant lines. 21 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "dry/monads"
  3. 1 module Milestoner
  4. 1 module Tags
  5. # Handles publishing of tags to a remote repository.
  6. 1 class Pusher
  7. 1 include Dependencies[:git, :logger]
  8. 1 include Dry::Monads[:result]
  9. 1 def call version
  10. 10 check_remote_repo(version).bind { check_remote_tag version }
  11. 3 .bind { push version }
  12. end
  13. 1 private
  14. 1 def check_remote_repo version
  15. 6 then: 4 else: 2 git.origin? ? Success(version) : Failure("Remote repository not configured.")
  16. end
  17. 1 def check_remote_tag version
  18. 4 then: 1 else: 3 git.tag_remote?(version) ? Failure("Remote tag exists: #{version}.") : Success(version)
  19. end
  20. 1 def push version
  21. 3 git.tags_push
  22. 2 .either proc { debug version },
  23. 1 proc { Failure "Tags could not be pushed to remote repository." }
  24. end
  25. 1 def debug version
  26. 4 logger.debug { "Local tag pushed: #{version}." }
  27. 2 Success version
  28. end
  29. end
  30. end
  31. end

lib/milestoner/views/context.rb

100.0% lines covered

100.0% branches covered

16 relevant lines. 16 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "forwardable"
  4. 1 require "hanami/view"
  5. 1 require "refinements/array"
  6. 1 module Milestoner
  7. 1 module Views
  8. # The view context.
  9. 1 class Context < Hanami::View::Context
  10. 1 extend Forwardable
  11. 1 include Dependencies[:settings]
  12. 1 using Refinements::Array
  13. 1 delegate %i[
  14. build_stylesheet
  15. generator_label
  16. generator_uri
  17. generator_version
  18. organization_label
  19. organization_uri
  20. project_author
  21. project_description
  22. project_label
  23. project_name
  24. project_uri_home
  25. project_uri_icon
  26. project_uri_logo
  27. project_uri_version
  28. project_version
  29. stylesheet_uri
  30. ] => :settings
  31. 1 def page_title delimiter: " | "
  32. 42 [project_title, organization_label].compress.join delimiter
  33. end
  34. 1 def project_slug
  35. 4 [project_name, project_version].compact.join("_").tr ".", Core::EMPTY_STRING
  36. end
  37. 1 def project_title = [project_label, project_version].compact.join " "
  38. end
  39. end
  40. end

lib/milestoner/views/milestones/index.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Milestoner
  4. 1 module Views
  5. 1 module Milestones
  6. # The index view.
  7. 1 class Index < Hanami::View
  8. 1 config.default_context = Context.new
  9. 1 config.part_namespace = Parts
  10. 1 config.paths = Container[:settings].build_template_paths
  11. 1 config.scope_namespace = Scopes
  12. 1 config.template = "milestones/index"
  13. 1 expose :tags
  14. end
  15. end
  16. end
  17. end

lib/milestoner/views/milestones/show.rb

100.0% lines covered

100.0% branches covered

12 relevant lines. 12 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Milestoner
  4. 1 module Views
  5. 1 module Milestones
  6. # The show view.
  7. 1 class Show < Hanami::View
  8. 1 include Dependencies[:settings]
  9. 1 config.default_context = Context.new
  10. 1 config.part_namespace = Parts
  11. 1 config.paths = Container[:settings].build_template_paths
  12. 1 config.scope_namespace = Scopes
  13. 1 config.template = "milestones/show"
  14. 1 expose :past, :tag, :future
  15. end
  16. end
  17. end
  18. end

lib/milestoner/views/parts/commit.rb

100.0% lines covered

100.0% branches covered

66 relevant lines. 66 lines covered and 0 lines missed.
38 total branches, 38 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 require "refinements/array"
  4. 1 module Milestoner
  5. 1 module Views
  6. 1 module Parts
  7. # The commit presentation logic.
  8. 1 class Commit < Hanami::View::Part
  9. 1 include Dependencies[:settings, :sanitizer, :color]
  10. 1 using Refinements::Array
  11. 1 decorate :author, as: :user
  12. 1 decorate :collaborators, as: :users
  13. 1 decorate :signers, as: :users
  14. 1 def initialize(**)
  15. 2033 super
  16. 2033 @prefixes = settings.commit_categories.pluck :label
  17. end
  18. 1 def colored_author(*custom)
  19. 10 then: 9 else: 1 custom.push :bold, :blue if custom.empty?
  20. 10 color[author.name, *custom]
  21. end
  22. 1 def colored_created_relative_at(*custom)
  23. 10 then: 9 else: 1 custom.push :bright_purple if custom.empty?
  24. 10 color[authored_relative_at, *custom]
  25. end
  26. 1 def colored_updated_relative_at(*custom)
  27. 10 then: 9 else: 1 custom.push :cyan if custom.empty?
  28. 10 color[committed_relative_at, *custom]
  29. end
  30. 1 def colored_sha(*custom)
  31. 10 then: 9 else: 1 custom.push :yellow if custom.empty?
  32. 10 color[sha[...12], *custom]
  33. end
  34. 1 def created_at_human = created_at.strftime "%Y-%m-%d (%A) %I:%M %p %Z"
  35. 1 def created_at_machine = created_at.strftime "%Y-%m-%dT%H:%M:%S%z"
  36. 1 def kind
  37. 134 then: 131 else: 3 if prefixes.include? prefix then "normal"
  38. 3 then: 1 else: 2 elsif directive? then "alert"
  39. 2 else "error"
  40. end
  41. end
  42. 1 def emoji
  43. 59 settings.commit_categories
  44. 75 .find { |category| category.fetch(:label) == prefix }
  45. 59 then: 57 else: 2 .then { |category| category ? category.fetch(:emoji) : "🔶" }
  46. end
  47. 1 def icon
  48. 105 then: 103 else: 2 if prefixes.include? prefix then String(prefix).downcase
  49. 2 then: 1 else: 1 elsif directive? then "rebase"
  50. 1 else "invalid"
  51. end
  52. end
  53. 1 def milestone_emoji
  54. 12 when: 1 case milestone
  55. 1 when: 1 when "major" then "🔴"
  56. 1 when: 9 when "minor" then "🔵"
  57. 9 else: 1 when "patch" then "🟢"
  58. 1 else "⚪️"
  59. end
  60. end
  61. 1 def safe_body = sanitizer.call(body_html).html_safe
  62. 1 def safe_notes = sanitizer.call(notes_html).html_safe
  63. 1 def total_deletions = format "%d", -deletions
  64. 1 def total_insertions
  65. 262 then: 128 else: 3 insertions.then { |total| total.positive? ? "+#{total}" : total.to_s }
  66. end
  67. 1 def tag
  68. 160 then: 1 else: 159 return "rebase" if directive?
  69. 159 else: 158 then: 1 return "invalid" unless prefixes.include? prefix
  70. 158 milestone
  71. end
  72. 1 def popover_id = "po-#{sha}"
  73. 59 then: 57 else: 1 def security = signature == "Good" ? "secure" : "insecure"
  74. 11 then: 9 else: 1 def signature_emoji = signature.then { |kind| kind == "Good" ? "🔒" : "🔓" }
  75. 3 then: 1 else: 1 def signature_id = value.fingerprint.then { |text| text.empty? ? "N/A" : text }
  76. 3 then: 1 else: 1 def signature_key = value.fingerprint_key.then { |text| text.empty? ? "N/A" : text }
  77. 1 def signature_label
  78. 60 then: 29 else: 1 signature.then { |kind| kind == "Good" ? "🔒 #{kind}" : "🔓 #{kind}" }
  79. end
  80. 1 def updated_at_human = updated_at.strftime "%Y-%m-%d (%A) %I:%M %p %Z"
  81. 1 def updated_at_machine = updated_at.strftime "%Y-%m-%dT%H:%M:%S%z"
  82. 1 private
  83. 1 attr_reader :prefixes
  84. end
  85. end
  86. end
  87. end

lib/milestoner/views/parts/tag.rb

100.0% lines covered

100.0% branches covered

44 relevant lines. 44 lines covered and 0 lines missed.
12 total branches, 12 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 require "refinements/array"
  4. 1 require "refinements/string"
  5. 1 module Milestoner
  6. 1 module Views
  7. 1 module Parts
  8. # The tag presentation logic.
  9. 1 class Tag < Hanami::View::Part
  10. 1 include Dependencies[:settings, :color, :durationer]
  11. 1 using Refinements::Array
  12. 1 using Refinements::String
  13. 1 decorate :commits
  14. 1 decorate :author, as: :user
  15. 1 decorate :contributors, as: :user
  16. 1 def colored_total_deletions(*custom)
  17. 11 then: 10 else: 1 custom.push :green if custom.empty?
  18. 11 color[total_deletions, *custom]
  19. end
  20. 1 def colored_total_insertions(*custom)
  21. 11 then: 10 else: 1 custom.push :red if custom.empty?
  22. 11 color[total_insertions, *custom]
  23. end
  24. 1 def commit_count = commits.size
  25. 1 def committed_at fallback: Time.now
  26. 338 then: 164 else: 5 value.committed_at.then { |at| at ? Time.at(at) : fallback }
  27. end
  28. 1 def committed_date = committed_at.strftime "%Y-%m-%d"
  29. 1 def committed_datetime = committed_at.strftime "%Y-%m-%dT%H:%M:%S%z"
  30. 1 def contributor_names = contributors.map(&:name).to_sentence
  31. 1 def deletion_count = commits.sum(&:deletions)
  32. 1 def duration
  33. 328 then: 11 else: 317 return 0 if commits.empty?
  34. 317 min = commits.min_by(&:created_at)
  35. 317 max = commits.max_by(&:updated_at)
  36. 317 (max.updated_at - min.created_at).to_i
  37. end
  38. 1 def empty? = commits.empty?
  39. 1 def file_count = commits.sum(&:files_changed)
  40. 1 def index? = settings.build_index
  41. 1 def insertion_count = commits.sum(&:insertions)
  42. 158 then: 145 else: 12 def security = signature ? "🔒 Tag (secure)" : "🔓 Tag (insecure)"
  43. 165 def total_commits = commit_count.then { |total| "#{total} commit".pluralize "s", total }
  44. 165 def total_files = file_count.then { |total| "#{total} file".pluralize "s", total }
  45. 1 def total_deletions
  46. 332 deletion_count.then { |total| "#{total} deletion".pluralize "s", total }
  47. end
  48. 165 then: 11 else: 153 def total_duration = duration.zero? ? "0 seconds" : durationer.call(duration)
  49. 1 def total_insertions
  50. 332 insertion_count.then { |total| "#{total} insertion".pluralize "s", total }
  51. end
  52. 1 def uri = format settings.project_uri_version, id: version
  53. end
  54. end
  55. end
  56. end

lib/milestoner/views/parts/user.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Milestoner
  4. 1 module Views
  5. 1 module Parts
  6. # The user presentation logic.
  7. 1 class User < Hanami::View::Part
  8. 1 include Dependencies[:settings]
  9. 744 def name = value.name.then { |text| text || "Unknown" }
  10. 657 def image_alt = value.name.then { |name| name || "missing" }
  11. 1 def avatar_url
  12. 656 value.name.then do |name|
  13. 656 then: 645 else: 11 return format settings.avatar_uri, id: value.external_id if name
  14. 11 "https://alchemists.io/images/projects/milestoner/icons/missing.png"
  15. end
  16. end
  17. 1 def profile_url
  18. 704 value.name.then do |name|
  19. 704 then: 685 else: 19 name ? format(settings.profile_uri, id: value.handle) : "/#unknown"
  20. end
  21. end
  22. end
  23. end
  24. end
  25. end

lib/milestoner/views/scopes/content.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "hanami/view"
  4. 1 module Milestoner
  5. 1 module Views
  6. 1 module Scopes
  7. # The content specific behavior for partials.
  8. 1 class Content < Hanami::View::Scope
  9. 1 def content = String locals.fetch(:content, Core::EMPTY_STRING)
  10. 262 then: 10 else: 251 def call = content.empty? ? render("milestones/none") : render("milestones/content")
  11. end
  12. end
  13. end
  14. end

lib/milestoner/views/scopes/logo.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "core"
  3. 1 require "hanami/view"
  4. 1 module Milestoner
  5. 1 module Views
  6. 1 module Scopes
  7. # The logo specific behavior for partials.
  8. 1 class Logo < Hanami::View::Scope
  9. 184 then: 8 else: 175 def call = project_uri_logo ? render("milestones/logo") : Core::EMPTY_STRING
  10. end
  11. end
  12. end
  13. end

lib/milestoner/views/scopes/tag_signature.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Milestoner
  4. 1 module Views
  5. 1 module Scopes
  6. # The tag signature specific behavior for partials.
  7. 1 class TagSignature < Hanami::View::Scope
  8. 1 def initialize(part: Parts::Tag.new(value: Models::Tag.new), **)
  9. 32 super(**)
  10. 32 @part = part
  11. end
  12. 1 def tag = locals.fetch :tag, part
  13. 1 def call
  14. 30 then: 28 else: 2 tag.signature ? render("milestones/tag-secure") : render("milestones/tag-insecure")
  15. end
  16. 1 private
  17. 1 attr_reader :part
  18. end
  19. end
  20. end
  21. end

lib/milestoner/views/scopes/users.rb

100.0% lines covered

100.0% branches covered

6 relevant lines. 6 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. # frozen_string_literal: true
  2. 1 require "hanami/view"
  3. 1 module Milestoner
  4. 1 module Views
  5. 1 module Scopes
  6. # The users specific behavior for partials.
  7. 1 class Users < Hanami::View::Scope
  8. 261 then: 247 else: 13 def call = users.any? ? render("milestones/users", users:) : render("milestones/none")
  9. end
  10. end
  11. end
  12. end