-
# frozen_string_literal: true
-
-
1
require "zeitwerk"
-
-
1
Zeitwerk::Loader.new.then do |loader|
-
1
loader.inflector.inflect "adoc" => "ADoc",
-
"ascii_doc" => "ASCIIDoc",
-
"cli" => "CLI",
-
"md" => "MD",
-
"uri" => "URI"
-
1
loader.tag = File.basename __FILE__, ".rb"
-
1
loader.push_dir __dir__
-
1
loader.setup
-
end
-
-
# Main namespace.
-
1
module Milestoner
-
1
def self.loader registry = Zeitwerk::Registry
-
6
@loader ||= registry.loaders.each.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module ADoc
-
# Defines ASCII Doc dependencies.
-
1
module Container
-
1
extend Containable
-
-
2
register(:indexer) { Indexer.new }
-
2
register(:pager) { Pager.new }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module ADoc
-
1
Dependencies = Infusible[Container]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module ADoc
-
# Builds ASCII Doc index.
-
1
class Indexer
-
1
include Milestoner::Dependencies[:settings, :logger]
-
-
1
def initialize(path_resolver: PathResolver, view: Views::Milestones::Index.new, **)
-
6
super(**)
-
6
@path_resolver = path_resolver
-
6
@view = view
-
end
-
-
1
def call tags
-
11
path_resolver.call settings.build_output.join("index.adoc"), logger: do |path|
-
11
path.write view.call(tags:, layout: settings.build_layout, format: :adoc)
-
end
-
end
-
-
1
private
-
-
1
attr_reader :path_resolver, :view
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module ADoc
-
# Builds ASCII Doc version.
-
1
class Pager
-
1
include Milestoner::Dependencies[:settings, :logger]
-
-
1
def initialize(path_resolver: PathResolver, view: Views::Milestones::Show.new, **)
-
20
super(**)
-
20
@path_resolver = path_resolver
-
20
@view = view
-
end
-
-
1
def call past, tag, future
-
27
settings.project_version = tag.version
-
27
write past, tag, future
-
end
-
-
1
private
-
-
1
attr_reader :path_resolver, :view
-
-
1
def write past, tag, future
-
27
path = settings.build_output.join(tag.version, settings.build_basename).sub_ext ".adoc"
-
-
27
path_resolver.call path, logger: do
-
27
path.write view.call(past:, tag:, future:, layout: settings.build_layout, format: :adoc)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
1
require "refinements/pathname"
-
-
1
module Milestoner
-
1
module Builders
-
# Builds ASCII Doc files.
-
1
class ASCIIDoc
-
1
include Milestoner::Dependencies[:settings, :logger]
-
1
include ADoc::Dependencies[:indexer, :pager]
-
-
1
using Refinements::Array
-
1
using Refinements::Pathname
-
-
1
def initialize(tagger: Tags::Enricher.new, view: Views::Milestones::Show.new, **)
-
7
super(**)
-
7
@tagger = tagger
-
7
@view = view
-
end
-
-
1
def call
-
8
tagger.call
-
6
.fmap { |tags| build tags }
-
2
.alt_map { |message| failure message }
-
end
-
-
1
private
-
-
1
attr_reader :tagger, :view
-
-
1
def build tags
-
6
indexer.call tags
-
14
tags.ring { |future, present, past| pager.call past, present, future }
-
6
settings.build_output
-
end
-
-
1
def failure message
-
4
logger.error { message }
-
2
message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "containable"
-
-
1
module Milestoner
-
1
module Builders
-
# Registers all builders for injection.
-
1
module Container
-
1
extend Containable
-
-
2
register(:ascii_doc) { ASCIIDoc.new }
-
2
register(:feed) { Feed.new }
-
2
register(:manifest) { Manifest.new }
-
2
register(:markdown) { Markdown.new }
-
2
register(:stream) { Stream.new }
-
2
register(:web) { Web.new }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "infusible"
-
-
1
module Milestoner
-
1
module Builders
-
1
Dependencies = Infusible[Container]
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/pathname"
-
-
1
module Milestoner
-
1
module Builders
-
# Builds syndicated feed output.
-
1
class Feed
-
1
include Milestoner::Dependencies[:logger]
-
-
1
using Refinements::Pathname
-
-
1
def initialize(tagger: Tags::Enricher.new, indexer: Syndication::Indexer.new, **)
-
5
super(**)
-
5
@tagger = tagger
-
5
@indexer = indexer
-
end
-
-
1
def call
-
6
tagger.call
-
4
.bind { |tags| indexer.call tags }
-
2
.alt_map { |message| failure message }
-
end
-
-
1
private
-
-
1
attr_reader :tagger, :indexer
-
-
1
def failure message
-
4
logger.error { message }
-
2
message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Builders
-
# Builds JSON manifest.
-
1
class Manifest
-
1
include Dry::Monads[:result]
-
1
include Milestoner::Dependencies[:settings, :git, :logger]
-
-
1
def initialize(writer: Tags::Manifest.new, path_resolver: PathResolver, **)
-
10
super(**)
-
10
@writer = writer
-
10
@path_resolver = path_resolver
-
end
-
-
1
def call
-
21
else: 7
then: 14
return Success writer.build_path unless settings.build_manifest
-
-
14
git.tags.either -> tags { write tags }, -> message { failure message }
-
end
-
-
1
private
-
-
1
attr_reader :writer, :path_resolver
-
-
1
def write tags
-
5
path_resolver.call writer.build_path, logger: do
-
4
versions = tags.map(&:version)
-
4
writer.write latest: versions.last, versions:
-
end
-
end
-
-
1
def failure message
-
4
logger.error { message }
-
2
Failure message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
1
require "refinements/pathname"
-
-
1
module Milestoner
-
1
module Builders
-
# Builds Markdown files.
-
1
class Markdown
-
1
include Milestoner::Dependencies[:settings, :logger]
-
1
include MD::Dependencies[:indexer, :pager]
-
-
1
using Refinements::Array
-
1
using Refinements::Pathname
-
-
1
def initialize(tagger: Tags::Enricher.new, view: Views::Milestones::Show.new, **)
-
7
super(**)
-
7
@tagger = tagger
-
7
@view = view
-
end
-
-
1
def call
-
8
tagger.call
-
6
.fmap { |tags| build tags }
-
2
.alt_map { |message| failure message }
-
end
-
-
1
private
-
-
1
attr_reader :tagger, :view
-
-
1
def build tags
-
6
indexer.call tags
-
14
tags.ring { |future, present, past| pager.call past, present, future }
-
6
settings.build_output
-
end
-
-
1
def failure message
-
4
logger.error { message }
-
2
message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module MD
-
# Defines Markdown dependencies.
-
1
module Container
-
1
extend Containable
-
-
2
register(:indexer) { Indexer.new }
-
2
register(:pager) { Pager.new }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module MD
-
1
Dependencies = Infusible[Container]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module MD
-
# Builds Markdown index.
-
1
class Indexer
-
1
include Milestoner::Dependencies[:settings, :logger]
-
-
1
def initialize(path_resolver: PathResolver, view: Views::Milestones::Index.new, **)
-
6
super(**)
-
6
@path_resolver = path_resolver
-
6
@view = view
-
end
-
-
1
def call tags
-
11
path_resolver.call settings.build_output.join("index.md"), logger: do |path|
-
11
path.write view.call(tags:, layout: settings.build_layout, format: :md)
-
end
-
end
-
-
1
private
-
-
1
attr_reader :path_resolver, :view
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module MD
-
# Builds Markdown version.
-
1
class Pager
-
1
include Milestoner::Dependencies[:settings, :logger]
-
-
1
def initialize(path_resolver: PathResolver, view: Views::Milestones::Show.new, **)
-
14
super(**)
-
14
@path_resolver = path_resolver
-
14
@view = view
-
end
-
-
1
def call past, tag, future
-
21
settings.project_version = tag.version
-
21
write past, tag, future
-
end
-
-
1
private
-
-
1
attr_reader :path_resolver, :view
-
-
1
def write past, tag, future
-
21
path = settings.build_output.join(tag.version, settings.build_basename).sub_ext ".md"
-
-
21
path_resolver.call path, logger: do
-
21
path.write view.call(past:, tag:, future:, layout: settings.build_layout, format: :md)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "refinements/pathname"
-
-
1
module Milestoner
-
# Safely handles file paths which may or may not exist.
-
1
module Builders
-
1
using Refinements::Pathname
-
-
1
PathResolver = lambda do |path, logger:, &block|
-
136
then: 2
if path.exist?
-
4
logger.warn { "Path exists: #{path}. Skipped." }
-
else: 134
else
-
134
path.make_ancestors
-
134
then: 131
else: 3
block.call path if block
-
268
logger.info { "Created: #{path}." }
-
end
-
-
136
Dry::Monads::Success path
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module Site
-
# Defines web dependencies.
-
1
module Container
-
1
extend Containable
-
-
2
register(:indexer) { Indexer.new }
-
2
register(:pager) { Pager.new }
-
2
register(:styler) { Styler.new }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module Site
-
1
Dependencies = Infusible[Container]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Builders
-
1
module Site
-
# Builds web index.
-
1
class Indexer
-
1
include Dry::Monads[:result]
-
1
include Milestoner::Dependencies[:settings, :logger]
-
-
1
def initialize(path_resolver: PathResolver, view: Views::Milestones::Index.new, **)
-
9
super(**)
-
9
@path_resolver = path_resolver
-
9
@view = view
-
end
-
-
1
def call tags
-
12
else: 10
then: 2
return Success() unless settings.build_index
-
-
10
path_resolver.call settings.build_output.join("index.html"), logger: do |path|
-
10
settings.project_version = nil
-
10
path.write view.call(tags:, layout: settings.build_layout)
-
end
-
end
-
-
1
private
-
-
1
attr_reader :path_resolver, :view
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module Site
-
# Builds web version.
-
1
class Pager
-
1
include Milestoner::Dependencies[:settings, :logger]
-
-
1
def initialize(path_resolver: PathResolver, view: Views::Milestones::Show.new, **)
-
21
super(**)
-
21
@path_resolver = path_resolver
-
21
@view = view
-
end
-
-
1
def call past, tag, future
-
26
settings.project_version = tag.version
-
26
write past, tag, future
-
end
-
-
1
private
-
-
1
attr_reader :path_resolver, :view
-
-
1
def write past, tag, future
-
26
path = settings.build_output.join(tag.version, settings.build_basename).sub_ext ".html"
-
-
26
path_resolver.call path, logger: do
-
26
path.write view.call(past:, tag:, future:, layout: settings.build_layout)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "refinements/pathname"
-
-
1
module Milestoner
-
1
module Builders
-
1
module Site
-
# Builds web stylesheet.
-
1
class Styler
-
1
include Milestoner::Dependencies[:settings, :logger]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Pathname
-
-
1
def initialize(path_resolver: PathResolver, **)
-
7
super(**)
-
7
@path_resolver = path_resolver
-
end
-
-
1
def call
-
10
else: 8
then: 2
return Success() unless settings.build_stylesheet
-
-
8
path = build_path
-
-
16
path_resolver.call(path, logger:) { copy path }
-
end
-
-
1
private
-
-
1
attr_reader :path_resolver
-
-
1
def build_path
-
8
path = Pathname settings.stylesheet_path
-
8
then: 1
else: 7
path.absolute? ? path : settings.build_output.join(path)
-
end
-
-
1
def copy build_path
-
8
settings.build_template_paths
-
32
.map { |template| template.join "public/page.css.erb" }
-
.find(&:exist?)
-
.copy build_path.make_ancestors
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
# Builds I/O stream output.
-
1
class Stream
-
1
include Milestoner::Dependencies[:settings, :logger, :io]
-
-
1
def initialize(tagger: Tags::Enricher.new, view: Views::Milestones::Show.new, **)
-
9
super(**)
-
9
@tagger = tagger
-
9
@view = view
-
end
-
-
1
def call
-
9
tagger.call
-
7
.fmap { |tags| write tags }
-
2
.alt_map { |message| failure message }
-
end
-
-
1
private
-
-
1
attr_reader :tagger, :view
-
-
8
def write(tags) = build(tags).tap { |content| io.write content }
-
-
1
def build tags
-
16
tags.reduce([]) { |content, tag| content.append render(tag) }
-
.join(%(\n#{"-" * 80}\n\n))
-
end
-
-
1
def render(tag) = view.call tag:, layout: settings.build_layout, format: :stream
-
-
1
def failure message
-
4
logger.error { message }
-
2
message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
1
require "rss"
-
-
1
module Milestoner
-
1
module Builders
-
1
module Syndication
-
# Builds feed in Atom format.
-
1
class Builder
-
1
include Milestoner::Dependencies[:settings]
-
1
include Dry::Monads[:result]
-
-
1
using Refine
-
-
1
def self.authors_for tags
-
123
tags.flat_map { |tag| tag.commits.map(&:author) }
-
44
then: 41
else: 3
.then { |users| users.any? ? users : tags.map(&:author) }
-
.uniq
-
end
-
-
1
def initialize(client: RSS::Maker, view: Views::Milestones::Show.new, **)
-
44
super(**)
-
44
@client = client
-
44
@view = view
-
end
-
-
1
def call tags
-
43
then: 1
else: 42
return Failure "No tags or commits." if tags.empty?
-
-
42
Success build_feed(tags).to_s
-
rescue NoMethodError, RSS::Error => error
-
2
Failure "#{self.class}: #{error.message.capitalize}."
-
end
-
-
1
private
-
-
1
attr_reader :client, :view
-
-
1
def build_feed tags
-
42
client.make "atom" do |node|
-
42
build_channel node.channel, tags
-
41
build_tags node, tags
-
end
-
end
-
-
1
def build_channel node, tags
-
42
at = tags.first.committed_at
-
-
42
node.merge id: settings.project_uri_home,
-
title: settings.syndication_label,
-
subtitle: settings.project_description,
-
icon: settings.project_uri_icon,
-
logo: settings.project_uri_logo,
-
rights: at.strftime("%Y"),
-
updated: at
-
-
41
build_channel_elements node, tags
-
end
-
-
1
def build_channel_elements node, tags
-
41
build_links node
-
41
build_generator node
-
41
build_authors node, self.class.authors_for(tags)
-
-
41
node.categories.build label: settings.project_label, term: settings.project_name
-
end
-
-
1
def build_links node
-
41
node.links.build_for settings.syndication_links,
-
label: :title,
-
uri: :href,
-
relation: :rel,
-
mime: :type
-
end
-
-
1
def build_generator node
-
41
node.generator do |generator|
-
41
generator.merge content: settings.generator_label,
-
version: settings.generator_version,
-
uri: settings.generator_uri
-
end
-
end
-
-
1
def build_tags node, tags = Core::EMPTY_ARRAY
-
116
tags.each { |tag| build_item node.items, tag }
-
end
-
-
1
def build_item node, tag
-
75
node.new_item do |item|
-
75
build_item_metadata item, tag
-
75
build_item_content item.content, tag
-
75
build_authors item, tag.commits.map(&:author).uniq.select(&:name)
-
75
item.categories.build_for settings.syndication_categories, label: :label, name: :term
-
end
-
end
-
-
1
def build_item_metadata node, tag
-
75
committed_at = tag.committed_at
-
75
version = tag.version
-
-
75
node.merge id: format(settings.syndication_id, id: version),
-
title: format(settings.syndication_entry_label, id: version),
-
link: format(settings.syndication_entry_uri, id: "#{version}/"),
-
rights: committed_at.strftime("%Y"),
-
published: committed_at,
-
updated: committed_at
-
end
-
-
1
def build_item_content node, tag
-
75
content = view.call tag:, layout: settings.build_layout, format: :xml
-
75
node.merge content:, type: :html
-
end
-
-
1
def build_authors node, users = Core::EMPTY_ARRAY
-
116
users.each do |user|
-
115
node.authors.build name: user.name, uri: format(settings.profile_uri, id: user.handle)
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module Syndication
-
# Builds syndicated feed output.
-
1
class Indexer
-
1
include Milestoner::Dependencies[:settings, :logger]
-
-
1
def initialize(path_resolver: PathResolver, syndicator: Syndication::Builder.new, **)
-
13
super(**)
-
13
@path_resolver = path_resolver
-
13
@syndicator = syndicator
-
end
-
-
13
def call(tags) = syndicator.call(tags).bind { |body| write body }
-
-
1
private
-
-
1
attr_reader :path_resolver, :syndicator
-
-
1
def write body
-
12
path = settings.build_output.join(settings.build_basename).sub_ext(".xml")
-
24
path_resolver.call(path, logger:) { path.write body }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "rss"
-
-
1
module Milestoner
-
1
module Builders
-
1
module Syndication
-
# Smooths out the rough edges of the RSS gem object which are harder to work with.
-
1
module Refine
-
2
refine(RSS::Maker::Atom::Feed::Channel) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Channel::Authors) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Channel::Authors::Author) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Channel::Categories) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Channel::Categories::Category) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Channel::Generator) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Channel::Links::Link) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Items::Item) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Channel::Links) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Items::Item::Authors) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Items::Item::Authors::Author) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Items::Item::Categories) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Items::Item::Categories::Category) { import_methods Shared }
-
2
refine(RSS::Maker::Atom::Feed::Items::Item::Content) { import_methods Shared }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Builders
-
1
module Syndication
-
# Provides shared functionality for refinements.
-
1
module Shared
-
1011
def merge(**attributes) = attributes.each { |key, value| public_send :"#{key}=", value }
-
-
1
def build_for(collection, **)
-
273
collection.each { |attributes| build(**attributes.transform_keys(**)) }
-
end
-
-
1
def build(**attributes)
-
313
node = public_send :"new_#{kind}"
-
1103
attributes.each { |key, value| node.public_send :"#{key}=", value }
-
end
-
-
1
private
-
-
1
def kind
-
313
self.class.name.downcase.split("::").last.sub("categories", "category").delete_suffix("s")
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
1
require "refinements/pathname"
-
-
1
module Milestoner
-
1
module Builders
-
# Builds web files,
-
1
class Web
-
1
include Milestoner::Dependencies[:settings, :logger]
-
1
include Site::Dependencies[:indexer, :pager, :styler]
-
-
1
using Refinements::Array
-
1
using Refinements::Pathname
-
-
1
def initialize(tagger: Tags::Enricher.new, **)
-
7
super(**)
-
7
@tagger = tagger
-
end
-
-
1
def call
-
6
tagger.call
-
4
.fmap { |tags| build tags }
-
2
.alt_map { |message| failure message }
-
end
-
-
1
private
-
-
1
attr_reader :tagger
-
-
1
def build tags
-
4
styler.call
-
4
indexer.call tags
-
10
tags.ring { |future, present, past| pager.call past, present, future }
-
4
settings.build_output
-
end
-
-
1
def failure message
-
4
logger.error { message }
-
2
message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "pathname"
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles output path.
-
1
class Basename < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set basename."
-
-
1
ancillary "The file extension is dynamically calculated from format."
-
-
1
on %w[-b --basename], argument: "[NAME]"
-
-
23
default { Container[:settings].build_basename }
-
-
1
def call(name = default) = settings.build_basename = name
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build output format.
-
1
class Format < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set output format."
-
-
1
on %w[-f --format], argument: "[KIND]", allow: %w[ascii_doc feed markdown stream web]
-
-
23
default { Container[:settings].build_format }
-
-
1
def call(kind = default) = settings.build_format = kind
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build index.
-
1
class Index < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Enable/disable versions index."
-
-
1
ancillary "Only used by web format."
-
-
1
on "--[no-]index"
-
-
23
default { Container[:settings].build_index }
-
-
1
def call(boolean) = settings.build_index = boolean
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build label.
-
1
class Label < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set label."
-
-
1
on %w[-l --label], argument: "[TEXT]"
-
-
23
default { Container[:settings].project_label }
-
-
1
def call(label = default) = settings.project_label = label
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build layout.
-
1
class Layout < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set view template layout."
-
-
1
ancillary "Use false to disable."
-
-
1
on %w[-L --layout], argument: "[NAME]"
-
-
24
default { Container[:settings].build_layout }
-
-
1
def call layout = default
-
3
then: 1
else: 2
settings.build_layout = layout == "false" ? false : layout
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build manifest.
-
1
class Manifest < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Enable/disable manifest."
-
-
1
on "--[no-]manifest"
-
-
23
default { Container[:settings].build_manifest }
-
-
1
def call(boolean) = settings.build_manifest = boolean
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build maximum.
-
1
class Max < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set maximum number of tags to process."
-
-
1
on %w[-m --max], argument: "[NUMBER]", type: Integer
-
-
23
default { Container[:settings].build_max }
-
-
1
def call(max = default) = settings.build_max = max
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "pathname"
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build output path.
-
1
class Output < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set output path."
-
-
1
on %w[-o --output], argument: "[PATH]"
-
-
24
default { Container[:settings].build_output }
-
-
1
def call(path = default) = settings.build_output = Pathname(path).expand_path
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build stylesheet.
-
1
class Stylesheet < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Enable/disable stylesheet."
-
-
1
ancillary "Only used by web format."
-
-
1
on "--[no-]stylesheet"
-
-
23
default { Container[:settings].build_stylesheet }
-
-
1
def call(boolean) = settings.build_stylesheet = boolean
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build tail.
-
1
class Tail < Sod::Action
-
1
include Dependencies[:settings]
-
-
1
description "Set tail reference."
-
-
1
ancillary "Defines the Git reference at which to cap the build."
-
-
1
on %w[-t --tail], argument: "[REFERENCE]", allow: %w[head tag]
-
-
23
default { Container[:settings].build_tail }
-
-
1
def call(reference = default) = settings.build_tail = reference
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
1
require "versionaire"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Build
-
# Handles build version.
-
1
class Version < Sod::Action
-
1
include Dependencies[:settings, :logger]
-
-
1
using Versionaire::Cast
-
-
1
description "Set version."
-
-
1
ancillary "Calculated from commit trailers when not supplied."
-
-
1
on %w[-v --version], argument: "[VERSION]"
-
-
24
default { Container[:settings].project_version }
-
-
1
def call version = default
-
3
settings.project_version = Version version
-
rescue Versionaire::Error => error
-
2
logger.error { error.message }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Cache
-
# Handles creating or updating a user within the cache.
-
1
class Create < Sod::Action
-
1
include Dependencies[:logger, client: :cache]
-
-
1
description "Create user."
-
-
1
ancillary %(Example: "1,zoe,Zoë Washburne".)
-
-
1
on %w[-c --create], argument: "external_id,handle,name"
-
-
1
def call values
-
6
case values.split ","
-
in: 2
in String => external_id, String => handle, String => name
-
4
client.write(:users) { upsert({external_id:, handle:, name:}) }
-
2
in: 1
.bind { |user| log_info "Created: #{user.name.inspect}" }
-
1
in: 1
in String, String then log_error "Name must be supplied."
-
1
in: 1
in [String] then log_error "Handle and Name must be supplied."
-
1
else: 1
in Core::EMPTY_ARRAY then log_error "No values given."
-
1
else log_error "Too many values given."
-
end
-
end
-
-
1
private
-
-
3
def log_info(message) = logger.info { message }
-
-
5
def log_error(message) = logger.error { message }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Cache
-
# Handles deleting a user from the cache.
-
1
class Delete < Sod::Action
-
1
include Dependencies[:logger, client: :cache]
-
-
1
description "Delete user."
-
-
1
on %w[-d --delete], argument: "NAME"
-
-
1
def call name
-
6
client.write(:users) { delete name }
-
.either(method(:success), method(:failure))
-
end
-
-
1
private
-
-
3
def success(user) = logger.info { "Deleted: #{user.name.inspect}." }
-
-
1
def failure(message) = logger.abort message
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Cache
-
# Handles finding a user in the cache.
-
1
class Find < Sod::Action
-
1
include Dependencies[:logger, :io, client: :cache]
-
-
1
description "Find user."
-
-
1
on %w[-f --find], argument: "NAME"
-
-
1
def call name
-
4
client.read(:users) { find name }
-
.either(method(:success), method(:failure))
-
end
-
-
1
private
-
-
1
def success(user) = io.puts user.to_h.values.join(", ")
-
-
1
def failure(message) = logger.abort message
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Cache
-
# Handles cache information.
-
1
class Info < Sod::Action
-
1
include Dependencies[:logger, client: :cache]
-
-
1
description "Show information."
-
-
1
on %w[-i --info]
-
-
1
def call(*)
-
2
path = client.path
-
2
then: 1
else: 1
path.exist? ? log_info("Path: #{path}.") : log_info("No cache found.")
-
end
-
-
1
private
-
-
3
def log_info(message) = logger.info { message }
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
1
module Cache
-
# Handles listing users within the cache.
-
1
class List < Sod::Action
-
1
include Dependencies[:logger, :io, client: :cache]
-
-
1
description "List users."
-
-
1
on %w[-l --list]
-
-
1
def call(*)
-
6
logger.info { "Listing users..." }
-
6
client.read(:users, &:all).bind { |users| print users }
-
end
-
-
1
private
-
-
1
def print users
-
5
then: 2
else: 1
return logger.info { "No users found." } if users.empty?
-
-
1
header
-
2
users.each { |user| io.puts user.to_h.values.map(&:inspect).join ", " }
-
end
-
-
1
def header
-
1
header = "External ID, Handle, Name"
-
-
1
io.puts header
-
1
io.puts "-" * header.size
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
# Handles calculation of next version.
-
1
class Next < Sod::Action
-
1
include Dependencies[:settings, :io]
-
-
1
description "Show next version."
-
-
1
ancillary "Calculated from commit trailers."
-
-
1
on %w[-n --next]
-
-
1
def call(*) = io.puts settings.project_version
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "sod"
-
1
require "versionaire"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Actions
-
# Handles tag creation and pushing of tag to local repository.
-
1
class Publish < Sod::Action
-
1
include Dependencies[:settings, :logger]
-
1
include Dry::Monads[:result]
-
-
1
using Versionaire::Cast
-
-
1
description "Publish milestone."
-
-
1
ancillary "Tag and push to remote repository."
-
-
1
on %w[-p --publish], argument: "[VERSION]"
-
-
17
default { Container[:settings].project_version }
-
-
1
def initialize(publisher: Tags::Publisher.new, **)
-
16
super(**)
-
16
@publisher = publisher
-
end
-
-
1
def call version = default
-
10
settings.build_max = 1
-
-
10
in: 3
case publisher.call Version(version)
-
3
in: 4
in Success(version) then version
-
4
else: 2
in Failure(message) then log_error message
-
2
else log_error "Publish failed, unable to parse result."
-
end
-
rescue Versionaire::Error => error
-
1
log_error error.message
-
end
-
-
1
private
-
-
1
attr_reader :publisher
-
-
1
def log_error message
-
14
logger.error { message }
-
7
message
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Commands
-
# Handles the building of different milestone formats.
-
1
class Build < Sod::Command
-
1
include Dependencies[:settings, :logger, :io]
-
1
include Builders::Dependencies[:ascii_doc, :feed, :manifest, :markdown, :stream, :web]
-
-
1
handle "build"
-
-
1
description "Build milestone."
-
-
1
on Actions::Build::Basename
-
1
on Actions::Build::Format
-
1
on Actions::Build::Index
-
1
on Actions::Build::Label
-
1
on Actions::Build::Layout
-
1
on Actions::Build::Manifest
-
1
on Actions::Build::Max
-
1
on Actions::Build::Output
-
1
on Actions::Build::Stylesheet
-
1
on Actions::Build::Tail
-
1
on Actions::Build::Version
-
-
1
def call
-
12
format = settings.build_format
-
-
12
log_info "Building #{settings.project_label} (#{format})..."
-
-
12
then: 11
if infused_keys.include? format.to_sym
-
11
__send__(format).call
-
else: 1
else
-
1
logger.abort "Invalid build format: #{format}."
-
end
-
-
12
manifest.call
-
end
-
-
1
private
-
-
12
def log_info(message) = logger.info { message }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
1
module Commands
-
# Handles the building of milestone output.
-
1
class Cache < Sod::Command
-
1
include Dependencies[:settings, :logger]
-
-
1
handle "cache"
-
-
1
description "Manage cache."
-
-
1
on Actions::Cache::Info
-
1
on Actions::Cache::List
-
1
on Actions::Cache::Find
-
1
on Actions::Cache::Create
-
1
on Actions::Cache::Delete
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "sod"
-
-
1
module Milestoner
-
1
module CLI
-
# The main Command Line Interface (CLI) object.
-
1
class Shell
-
1
include Dependencies[:defaults_path, :specification, xdg_config: "xdg.config"]
-
-
1
def initialize(context: Sod::Context, dsl: Sod, **)
-
8
super(**)
-
8
@context = context
-
8
@dsl = dsl
-
end
-
-
1
def call(...) = cli.call(...)
-
-
1
private
-
-
1
attr_reader :context, :dsl
-
-
1
def cli
-
8
context = build_context
-
-
8
dsl.new :milestoner, banner: specification.banner do
-
8
on(Sod::Prefabs::Commands::Config, context:)
-
8
on Commands::Cache
-
8
on Commands::Build
-
8
on Actions::Next
-
8
on Actions::Publish
-
8
on(Sod::Prefabs::Actions::Version, context:)
-
8
on Sod::Prefabs::Actions::Help, self
-
end
-
end
-
-
1
def build_context
-
8
context[defaults_path:, xdg_config:, version_label: specification.labeled_version]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "refinements/array"
-
-
1
module Milestoner
-
1
module Commits
-
# Retrieves and categorizes Git repository commit tagged or untagged history.
-
1
class Categorizer
-
1
include Dependencies[:settings]
-
-
1
using Refinements::Array
-
-
1
def initialize(collector: Collector.new, **)
-
37
super(**)
-
-
37
@collector = collector
-
37
@labels = settings.commit_categories.pluck :label
-
37
then: 1
else: 36
@pattern = labels.empty? ? // : Regexp.union(labels)
-
end
-
-
1
def call min: Collector::MIN, max: Collector::MAX
-
359
collect(min, max).each_value { |commits| commits.sort_by! { |_, commit| commit.subject } }
-
.values
-
.reduce(&:concat)
-
end
-
-
1
private
-
-
1
attr_reader :collector, :labels, :pattern
-
-
1
def collect min, max
-
40
collector.call(min:, max:)
-
.value_or(Core::EMPTY_ARRAY)
-
.each
-
.with_index(1)
-
.with_object categories do |(commit, position), collection|
-
86
category = commit.subject[pattern]
-
86
then: 80
else: 6
key = collection.key?(category) ? category : "Unknown"
-
86
collection[key] << [position, commit]
-
end
-
end
-
-
1
def categories
-
233
labels.reduce({}) { |group, prefix| group.merge prefix => [] }
-
.merge! "Unknown" => []
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Commits
-
# Collects commits since last tag, a specific range, or all commits if untagged.
-
1
class Collector
-
1
include Dry::Monads[:result]
-
1
include Dependencies[:git]
-
-
1
MIN = :last
-
1
MAX = :HEAD
-
-
64
then: 32
else: 31
def call(min: MIN, max: MAX) = git.tagged? ? slice(min, max) : all
-
-
1
private
-
-
1
def slice min, max
-
32
in: 10
case [min, max]
-
20
in: 16
in MIN, MAX then git.tag_last.bind { |tag| git.commits "#{tag}..#{max}" }
-
16
in: 5
in String, String then git.commits "#{min}..#{max}"
-
5
else: 1
in nil, String then git.commits max
-
1
else Failure "Invalid minimum and/or maximum range: #{min}..#{max}."
-
end
-
end
-
-
1
def all = git.commits
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Commits
-
# Enriches commits and associated trailers for final processing.
-
1
class Enricher
-
1
include Dependencies[:settings]
-
-
1
include Enrichers::Dependencies[
-
:author,
-
:body_html,
-
:collaborators,
-
:created_at,
-
:format,
-
:issue,
-
:milestone,
-
:notes_html,
-
:review,
-
:signers,
-
:updated_at,
-
:uri
-
]
-
-
1
include Dry::Monads[:result]
-
-
1
def initialize(categorizer: Commits::Categorizer.new, model: Models::Commit, **)
-
33
super(**)
-
33
@categorizer = categorizer
-
33
@model = model
-
end
-
-
1
def call min: Collector::MIN, max: Collector::MAX
-
36
categorizer.call(min:, max:)
-
70
.map { |(position, commit)| build_record position, commit }
-
36
.then { |commits| Success commits }
-
end
-
-
1
private
-
-
1
attr_reader :categorizer, :model
-
-
1
def build_record(position, commit) = model.for commit, position:, **build_attributes(commit)
-
-
1
def build_attributes commit
-
70
infused_keys.each.with_object({}) do |key, attributes|
-
840
attributes[key] = __send__(key).call commit
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches a commit author by using cache.
-
1
class Author
-
1
include Milestoner::Dependencies[:cache]
-
-
1
def initialize(model: Models::User, **)
-
3
super(**)
-
3
@model = model
-
end
-
-
1
def call commit
-
198
cache.read(:users) { |table| table.find commit.author_name }
-
.value_or(model.new)
-
end
-
-
1
private
-
-
1
attr_reader :model
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches commit text by rendering as HTML based on trailer information.
-
1
class Body
-
1
include Milestoner::Dependencies[:settings]
-
-
1
def initialize(key: "Format", renderer: Renderers::Universal.new, **)
-
3
super(**)
-
3
@key = key
-
3
@renderer = renderer
-
end
-
-
1
def call commit
-
72
commit.trailer_value_for(key)
-
.value_or(settings.commit_format)
-
72
.then { |format| renderer.call commit.body, for: format.to_sym }
-
end
-
-
1
private
-
-
1
attr_reader :key, :renderer
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "gitt"
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches a commit colleague by using cache.
-
1
class Colleague
-
1
include Milestoner::Dependencies[:cache]
-
-
1
def initialize(key:, parser: Gitt::Parsers::Person.new, **)
-
5
super(**)
-
5
@key = key
-
5
@parser = parser
-
end
-
-
144
def call(commit) = commit.find_trailers(key).bind { |trailers| users_for(trailers).compact }
-
-
1
private
-
-
1
attr_reader :key, :parser
-
-
5
def users_for(trailers) = trailers.map { |trailer| user_for parser.call(trailer.value) }
-
-
1
def user_for person
-
8
cache.read(:users) { find person.name }
-
.value_or(nil)
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "containable"
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Registers all enrichers for injection.
-
1
module Container
-
1
extend Containable
-
-
2
register(:author) { Author.new }
-
2
register(:body_html) { Body.new }
-
2
register(:collaborators) { Colleague.new key: "Co-authored-by" }
-
2
register(:created_at) { Time.new key: :authored_at }
-
2
register(:format) { Format.new }
-
2
register(:issue) { Issue.new }
-
2
register(:milestone) { Milestone.new }
-
2
register(:notes_html) { Note.new }
-
2
register(:review) { Review.new }
-
2
register(:signers) { Colleague.new key: "Signed-off-by" }
-
2
register(:updated_at) { Time.new key: :committed_at }
-
2
register(:uri) { URI.new }
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "infusible"
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
1
Dependencies = Infusible[Container]
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches a commit format based on trailer information.
-
1
class Format
-
1
include Milestoner::Dependencies[:settings]
-
-
1
def initialize(key: "Format", **)
-
3
super(**)
-
3
@key = key
-
end
-
-
1
def call(commit) = commit.trailer_value_for(key).value_or(settings.commit_format)
-
-
1
private
-
-
1
attr_reader :key
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches a commit issue based on trailer information.
-
1
class Issue
-
1
include Milestoner::Dependencies[:settings]
-
-
1
def initialize(key: "Issue", model: Models::Link, **)
-
3
super(**)
-
3
@key = key
-
3
@model = model
-
end
-
-
1
def call commit
-
72
uri = settings.tracker_uri
-
-
72
commit.trailer_value_for(key)
-
2
.either -> value { model[id: value, uri: format(uri, id: value)] },
-
70
proc { model[id: "All", uri: format(uri, id: nil)] }
-
end
-
-
1
private
-
-
1
attr_reader :key, :model
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches a commit milestone based on trailer information.
-
1
class Milestone
-
1
include Milestoner::Dependencies[:settings]
-
-
1
def initialize(key: "Milestone", default: "unknown", **)
-
3
super(**)
-
3
@key = key
-
3
@default = default
-
end
-
-
1
def call(commit) = commit.trailer_value_for(key).value_or(default)
-
-
1
private
-
-
1
attr_reader :key, :default
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches commit notes by rendering as HTML based on trailer information.
-
1
class Note
-
1
include Milestoner::Dependencies[:settings]
-
-
1
def initialize(key: "Format", renderer: Renderers::Universal.new, **)
-
3
super(**)
-
3
@key = key
-
3
@renderer = renderer
-
end
-
-
1
def call commit
-
72
commit.trailer_value_for(key)
-
.value_or(settings.commit_format)
-
72
.then { |format| renderer.call commit.notes, for: format.to_sym }
-
end
-
-
1
private
-
-
1
attr_reader :key, :renderer
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches a commit review based on trailer information.
-
1
class Review
-
1
include Milestoner::Dependencies[:settings]
-
-
1
def initialize(model: Models::Link, **)
-
2
super(**)
-
2
@model = model
-
end
-
-
1
def call(*) = model[id: "All", uri: format(settings.review_uri, id: nil)]
-
-
1
private
-
-
1
attr_reader :model
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches raw time as a Time instance.
-
1
class Time
-
1
def initialize key:
-
4
@key = key
-
end
-
-
1
def call(commit) = ::Time.at commit.public_send(key).to_i
-
-
1
private
-
-
1
attr_reader :key
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Commits
-
1
module Enrichers
-
# Enriches a commit URI based on trailer information.
-
1
class URI
-
1
include Milestoner::Dependencies[:settings]
-
-
1
def call(commit) = format settings.commit_uri, id: commit.sha
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
1
require "versionaire"
-
-
1
module Milestoner
-
1
module Commits
-
# Calculates next version based on commit trailer version keys.
-
1
class Versioner
-
1
include Dependencies[:git, :logger]
-
1
include Dry::Monads[:result]
-
-
1
using Versionaire::Cast
-
-
1
DEFAULTS = {trailer_key: "Milestone", fallback: Versionaire::Version.new}.freeze
-
-
1
def initialize(defaults: DEFAULTS, collector: Collector.new, **)
-
434
super(**)
-
434
@defaults = defaults
-
434
@collector = collector
-
end
-
-
1
def call
-
20
trailer_milestones.then { |milestones| bump milestones }
-
.value_or(fallback)
-
end
-
-
1
private
-
-
1
attr_reader :defaults, :collector
-
-
1
def trailer_milestones
-
10
collector.call.value_or(Core::EMPTY_ARRAY).each.with_object [] do |commit, values|
-
63
commit.trailer_value_for(trailer_key).bind { |milestone| values.append milestone.to_sym }
-
end
-
end
-
-
1
def bump milestones
-
10
last_tag_or_fallback_for milestones
-
rescue Versionaire::Error => error
-
4
logger.debug { error.message }
-
2
Failure error
-
end
-
-
1
def last_tag_or_fallback_for milestones
-
10
target = fallback.members.intersection(milestones).first
-
18
then: 7
else: 1
git.tag_last.fmap { |tag| target ? Version(tag).bump(target) : fallback }
-
end
-
-
1
def trailer_key = defaults.fetch __method__
-
-
1
def fallback = defaults.fetch __method__
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/schema"
-
1
require "etcher"
-
-
1
Dry::Schema.load_extensions :monads
-
-
1
module Milestoner
-
1
module Configuration
-
1
Contract = Dry::Schema.Params do
-
1
required(:avatar_uri).filled :string
-
1
required(:build_basename).filled :string
-
1
required(:build_format).filled :string
-
1
required(:build_index).filled :bool
-
2
required(:build_layout) { str? | bool? }
-
1
required(:build_max).filled :integer
-
1
required(:build_manifest).filled :bool
-
1
required(:build_output).filled Etcher::Types::Pathname
-
1
required(:build_stylesheet).filled :bool
-
1
required(:build_tail).filled :string
-
1
required(:build_template_paths).array Etcher::Types::Pathname
-
-
1
required(:commit_categories).array(:hash) do
-
1
required(:emoji).filled :string
-
1
required(:label).filled :string
-
end
-
-
1
required(:commit_format).filled :string
-
1
required(:commit_uri).filled :string
-
1
required(:generator_label).filled :string
-
1
required(:generator_uri).filled :string
-
1
required(:generator_version).filled Etcher::Types::Version
-
1
required(:loaded_at).filled :time
-
1
required(:organization_label).filled :string
-
1
required(:organization_uri).filled :string
-
1
required(:profile_uri).filled :string
-
1
required(:project_author).filled :string
-
1
optional(:project_description).filled :string
-
1
optional(:project_label).filled :string
-
1
required(:project_name).filled :string
-
1
required(:project_owner).filled :string
-
1
required(:project_uri_home).filled :string
-
1
optional(:project_uri_icon).filled :string
-
1
optional(:project_uri_logo).filled :string
-
1
required(:project_uri_version).filled :string
-
1
required(:project_version).filled Etcher::Types::Version
-
1
required(:review_uri).filled :string
-
1
required(:stylesheet_path).filled :string
-
1
required(:stylesheet_uri).filled :string
-
-
1
required(:syndication_categories).array(:hash) do
-
1
required(:label).filled :string
-
1
required(:name).filled :string
-
end
-
-
1
required(:syndication_entry_label).filled :string
-
1
required(:syndication_entry_uri).filled :string
-
1
required(:syndication_id).filled :string
-
1
required(:syndication_label).filled :string
-
-
1
required(:syndication_links).array(:hash) do
-
1
required(:label).filled :string
-
1
required(:mime).filled :string
-
1
required(:relation).filled :string
-
1
required(:uri).filled :string
-
end
-
-
1
required(:tag_subject).filled :string
-
1
required(:tracker_uri).filled :string
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Configuration
-
# Defines configuration content as the primary source of truth for use throughout the gem.
-
1
Model = Struct.new :avatar_uri,
-
:build_basename,
-
:build_format,
-
:build_index,
-
:build_layout,
-
:build_manifest,
-
:build_max,
-
:build_output,
-
:build_stylesheet,
-
:build_tail,
-
:build_template_paths,
-
:commit_categories,
-
:commit_format,
-
:commit_uri,
-
:generator_label,
-
:generator_uri,
-
:generator_version,
-
:loaded_at,
-
:organization_label,
-
:organization_uri,
-
:profile_uri,
-
:project_author,
-
:project_description,
-
:project_label,
-
:project_name,
-
:project_owner,
-
:project_uri_home,
-
:project_uri_icon,
-
:project_uri_logo,
-
:project_uri_version,
-
:project_version,
-
:review_uri,
-
:stylesheet_path,
-
:stylesheet_uri,
-
:syndication_categories,
-
:syndication_entry_label,
-
:syndication_entry_uri,
-
:syndication_id,
-
:syndication_label,
-
:syndication_links,
-
:tag_subject,
-
:tracker_uri
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "pathname"
-
1
require "runcom"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Build
-
# Ensures XDG configuration and gem template paths are configured.
-
1
class TemplatePaths
-
1
include Dry::Monads[:result]
-
-
1
def initialize key = :build_template_paths,
-
default: Pathname(__dir__).join("../../../templates"),
-
xdg: Runcom::Config.new("milestoner/templates")
-
425
@key = key
-
425
@default = default
-
425
@xdg = xdg
-
end
-
-
1
def call(attributes) = Success attributes.merge!(key => xdg.all.append(default))
-
-
1
private
-
-
1
attr_reader :key, :default, :xdg
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "cff"
-
1
require "dry/monads"
-
1
require "pathname"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Citations
-
# Conditionally updates project description based on citation details.
-
1
class Description
-
1
include Dry::Monads[:result]
-
-
1
def initialize key = :project_description,
-
path: Pathname.pwd.join("CITATION.cff"),
-
citation: CFF::File
-
427
@key = key
-
427
@path = path
-
427
@citation = citation
-
end
-
-
1
def call attributes
-
427
attributes.fetch key do
-
426
value = citation.open(path).abstract
-
426
else: 1
then: 425
attributes.merge! key => value unless value.empty?
-
end
-
-
427
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key, :path, :citation
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "cff"
-
1
require "dry/monads"
-
1
require "pathname"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Citations
-
# Conditionally updates project label based on citation details.
-
1
class Label
-
1
include Dry::Monads[:result]
-
-
1
def initialize key = :project_label,
-
path: Pathname.pwd.join("CITATION.cff"),
-
citation: CFF::File
-
427
@key = key
-
427
@path = path
-
427
@citation = citation
-
end
-
-
1
def call attributes
-
427
attributes.fetch key do
-
426
value = citation.open(path).title
-
426
else: 1
then: 425
attributes.merge! key => value unless value.empty?
-
end
-
-
427
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key, :path, :citation
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "cff"
-
1
require "dry/monads"
-
1
require "pathname"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Citations
-
# Conditionally updates project URI based on citation URL.
-
1
class URI
-
1
include Dry::Monads[:result]
-
-
1
def initialize key = :project_uri,
-
path: Pathname.pwd.join("CITATION.cff"),
-
citation: CFF::File
-
429
@key = key
-
429
@path = path
-
429
@citation = citation
-
end
-
-
1
def call attributes
-
429
process attributes
-
428
Success attributes
-
rescue KeyError => error
-
1
Failure step: :transform,
-
payload: "Unable to transform #{key.inspect}, missing specifier: " \
-
"\"#{error.message[/<.+>/]}\"."
-
end
-
-
1
private
-
-
1
attr_reader :key, :path, :citation
-
-
1
def process attributes
-
855
attributes.fetch(key) { citation.open(path).url }
-
429
then: 2
else: 427
.then { |value| value.match?(/%<.+>s/) ? format(value, attributes) : value }
-
428
else: 1
then: 427
.then { |value| attributes.merge! key => value unless value.empty? }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "pathname"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Gems
-
# Conditionally updates project description based on specification summary.
-
1
class Description
-
1
include Dependencies[:spec_loader]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(key = :project_description, path: "#{Pathname.pwd.basename}.gemspec", **)
-
427
super(**)
-
427
@key = key
-
427
@path = path
-
end
-
-
1
def call attributes
-
427
attributes.fetch key do
-
426
value = spec_loader.call(path).summary
-
426
then: 1
else: 425
attributes.merge! key => value if value
-
end
-
-
427
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key, :path
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "pathname"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Gems
-
# Conditionally updates project label based on specification label.
-
1
class Label
-
1
include Dependencies[:spec_loader]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(key = :project_label, path: "#{Pathname.pwd.basename}.gemspec", **)
-
427
super(**)
-
427
@key = key
-
427
@path = path
-
end
-
-
1
def call attributes
-
427
attributes.fetch key do
-
426
value = spec_loader.call(path).label
-
426
else: 425
then: 1
attributes.merge! key => value unless value.empty?
-
end
-
-
427
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key, :path
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "pathname"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Gems
-
# Conditionally updates project name based on specification name.
-
1
class Name
-
1
include Dependencies[:spec_loader]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(key = :project_name, path: "#{Pathname.pwd.basename}.gemspec", **)
-
427
super(**)
-
427
@key = key
-
427
@path = path
-
end
-
-
1
def call attributes
-
427
attributes.fetch key do
-
3
value = spec_loader.call(path).name
-
3
then: 1
else: 2
attributes.merge! key => value if value
-
end
-
-
427
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key, :path
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "pathname"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Gems
-
# Conditionally updates project URI based on specification home page URL.
-
1
class URI
-
1
include Dependencies[:spec_loader]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(key = :project_uri, path: "#{Pathname.pwd.basename}.gemspec", **)
-
429
super(**)
-
429
@key = key
-
429
@path = path
-
end
-
-
1
def call attributes
-
429
process attributes
-
428
Success attributes
-
rescue KeyError => error
-
1
Failure step: :transform,
-
payload: "Unable to transform #{key.inspect}, missing specifier: " \
-
"\"#{error.message[/<.+>/]}\"."
-
end
-
-
1
private
-
-
1
attr_reader :key, :path
-
-
1
def process attributes
-
855
attributes.fetch(key) { spec_loader.call(path).homepage_url }
-
429
then: 2
else: 427
.then { |value| value.match?(/%<.+>s/) ? format(value, attributes) : value }
-
428
else: 425
then: 3
.then { |value| attributes.merge! key => value unless value.empty? }
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Generator
-
# Conditionally updates generator label based on gem specification.
-
1
class Label
-
1
include Dependencies[:specification]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(key = :generator_label, **)
-
426
super(**)
-
426
@key = key
-
end
-
-
1
def call attributes
-
851
attributes.fetch(key) { attributes.merge! key => specification.label }
-
426
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Generator
-
# Conditionally updates generator URI based on gem specification.
-
1
class URI
-
1
include Dependencies[:specification]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(key = :generator_uri, **)
-
426
super(**)
-
426
@key = key
-
end
-
-
1
def call attributes
-
851
attributes.fetch(key) { attributes.merge! key => specification.homepage_url }
-
426
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Generator
-
# Conditionally updates generator version based on gem specification.
-
1
class Version
-
1
include Dependencies[:specification]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(key = :generator_version, **)
-
426
super(**)
-
426
@key = key
-
end
-
-
1
def call attributes
-
851
attributes.fetch(key) { attributes.merge! key => specification.version }
-
426
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Project
-
# Conditionally updates author based on Git user.
-
1
class Author
-
1
include Dependencies[:git]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(key = :project_author, **)
-
428
super(**)
-
428
@key = key
-
end
-
-
1
def call attributes
-
428
attributes.fetch key do
-
853
then: 425
else: 1
git.get("user.name", nil).bind { |value| attributes.merge! key => value if value }
-
end
-
-
428
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "pathname"
-
1
require "refinements/string"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
# Conditionally updates label based on current directory.
-
1
module Project
-
1
using Refinements::String
-
-
1
Label = lambda do |attributes, key = :project_label, default: Pathname.pwd.basename.to_s|
-
429
attributes.fetch(key) { attributes.merge! key => default.titleize }
-
427
Dry::Monads::Success attributes
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "refinements/hash"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
1
module Project
-
# Conditionally updates version based on last Git tag.
-
1
class Version
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Hash
-
-
1
def initialize key = :project_version, versioner: Commits::Versioner.new
-
426
@key = key
-
426
@versioner = versioner
-
end
-
-
1
def call attributes
-
428
attributes.fetch_value(key) { attributes.merge! key => versioner.call }
-
426
Success attributes
-
end
-
-
1
private
-
-
1
attr_reader :key, :versioner
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
1
require "refinements/hash"
-
-
1
module Milestoner
-
1
module Configuration
-
1
module Transformers
-
# Conditionally updates links based on project details.
-
1
module Syndication
-
1
using Refinements::Hash
-
-
1
Link = lambda do |attributes, key = :syndication_links|
-
429
links = attributes.fetch key, Core::EMPTY_ARRAY
-
-
429
links.each do |link|
-
854
link.symbolize_keys!
-
-
854
link[:label] = format link[:label], attributes
-
853
link[:uri] = format link[:uri], attributes
-
end
-
-
427
Dry::Monads::Success attributes.merge!(key => links)
-
rescue KeyError => error
-
2
Dry::Monads::Failure step: :transform,
-
payload: "Unable to transform #{key.inspect}, missing specifier: " \
-
"\"#{error.message[/<.+>/]}\"."
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "cogger"
-
1
require "containable"
-
1
require "etcher"
-
1
require "gitt"
-
1
require "lode"
-
1
require "runcom"
-
1
require "spek"
-
1
require "tone"
-
-
1
module Milestoner
-
# Provides a global gem container for injection into other objects.
-
1
module Container
-
1
extend Containable
-
-
1
namespace :xdg do
-
1
register(:cache) { Runcom::Cache.new "milestoner/database.store" }
-
2
register(:config) { Runcom::Config.new "milestoner/configuration.yml" }
-
end
-
-
1
register :cache do
-
skipped
# :nocov:
-
skipped
Lode.new self["xdg.cache"].passive do |config|
-
skipped
config.mode = :thread
-
skipped
config.table = Lode::Tables::Value
-
skipped
config.register :users, model: Models::User, primary_key: :name
-
skipped
end
-
skipped
end
-
skipped
-
skipped
register :registry, as: :fresh do
-
skipped
Etcher::Registry.new(contract: Configuration::Contract, model: Configuration::Model)
-
skipped
.add_loader(:yaml, self[:defaults_path])
-
skipped
.add_loader(:yaml, self["xdg.config"].active)
-
skipped
.add_transformer(:root, :build_output)
-
skipped
.add_transformer(Configuration::Transformers::Build::TemplatePaths.new)
-
skipped
.add_transformer(Configuration::Transformers::Gems::Label.new)
-
skipped
.add_transformer(Configuration::Transformers::Gems::Description.new)
-
skipped
.add_transformer(Configuration::Transformers::Gems::Name.new)
-
skipped
.add_transformer(Configuration::Transformers::Gems::URI.new)
-
skipped
.add_transformer(Configuration::Transformers::Citations::Label.new)
-
skipped
.add_transformer(Configuration::Transformers::Citations::Description.new)
-
skipped
.add_transformer(Configuration::Transformers::Citations::URI.new)
-
skipped
.add_transformer(Configuration::Transformers::Project::Author.new)
-
skipped
.add_transformer(Configuration::Transformers::Project::Label)
-
skipped
.add_transformer(:basename, :project_name)
-
skipped
.add_transformer(Configuration::Transformers::Project::Version.new)
-
skipped
.add_transformer(:format, :project_uri_home)
-
skipped
.add_transformer(:format, :project_uri_icon)
-
skipped
.add_transformer(:format, :project_uri_logo)
-
skipped
.add_transformer(:format, :project_uri_version, id: "%<id>s")
-
skipped
.add_transformer(Configuration::Transformers::Generator::Label.new)
-
skipped
.add_transformer(Configuration::Transformers::Generator::URI.new)
-
skipped
.add_transformer(Configuration::Transformers::Generator::Version.new)
-
skipped
.add_transformer(:format, :syndication_entry_label, id: "%<id>s")
-
skipped
.add_transformer(:format, :syndication_entry_uri, id: "%<id>s")
-
skipped
.add_transformer(:format, :syndication_id, id: "%<id>s")
-
skipped
.add_transformer(:format, :syndication_label)
-
skipped
.add_transformer(Configuration::Transformers::Syndication::Link)
-
skipped
.add_transformer(:format, :commit_uri, id: "%<id>s")
-
skipped
.add_transformer(:format, :review_uri, id: "%<id>s")
-
skipped
.add_transformer(:format, :tracker_uri, id: "%<id>s")
-
skipped
.add_transformer(:time, :loaded_at)
-
skipped
end
-
skipped
-
skipped
register(:settings) { Etcher.call(self[:registry]).dup }
-
skipped
register(:specification) { self[:spec_loader].call "#{__dir__}/../../milestoner.gemspec" }
-
skipped
register(:sanitizer) { Sanitizer.new }
-
skipped
register(:spec_loader) { Spek::Loader.new }
-
skipped
register(:git) { Gitt::Repository.new }
-
skipped
register(:defaults_path) { Pathname(__dir__).join("configuration/defaults.yml") }
-
skipped
register(:color) { Tone.new }
-
skipped
register :durationer, Durationer
-
skipped
register(:logger) { Cogger.new id: :milestoner }
-
skipped
register :io, STDOUT
-
skipped
end
-
skipped
end
-
# frozen_string_literal: true
-
-
1
require "infusible"
-
-
1
module Milestoner
-
1
Dependencies = Infusible[Container]
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
1
require "refinements/string"
-
-
# Computes duration (in seconds) into human readable years, days, hours, minutes, and seconds.
-
1
module Milestoner
-
1
using Refinements::Array
-
1
using Refinements::String
-
-
1
DURATION_UNITS = {
-
"year" => 31_536_000, # 60 * 60 * 25 * 365
-
"day" => 86_400, # 60 * 60 * 25
-
"hour" => 3_600, # 60 * 60
-
"minute" => 60,
-
"second" => 1
-
}.freeze
-
-
1
Durationer = lambda do |seconds, units: DURATION_UNITS|
-
161
then: 2
else: 159
return "0 seconds" if seconds.negative? || seconds.zero?
-
-
159
result = units.map do |unit, divisor|
-
795
count, seconds = seconds.divmod divisor
-
-
795
then: 324
else: 471
next if count.zero?
-
-
471
%(#{count} #{unit.pluralize "s", count})
-
end
-
-
159
result.compact.to_sentence
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "gitt"
-
-
1
module Milestoner
-
1
module Models
-
1
COMMIT_COMMON_ATTRIBUTES = %i[
-
authored_at
-
authored_relative_at
-
body
-
committed_at
-
committed_relative_at
-
deletions
-
encoding
-
files_changed
-
fingerprint
-
fingerprint_key
-
insertions
-
notes
-
sha
-
signature
-
subject
-
].freeze
-
-
1
COMMIT_ENRICHED_ATTRIBUTES = %i[
-
author
-
body_html
-
collaborators
-
created_at
-
format
-
issue
-
milestone
-
notes_html
-
position
-
review
-
signers
-
updated_at
-
uri
-
].freeze
-
-
# Represents an enriched commit.
-
1
Commit = Struct.new(*COMMIT_COMMON_ATTRIBUTES, *COMMIT_ENRICHED_ATTRIBUTES) do
-
1
include Gitt::Directable
-
-
1
def self.for(commit, **) = new(**commit.to_h.slice(*COMMIT_COMMON_ATTRIBUTES), **)
-
-
1
def initialize(**)
-
296
super
-
296
freeze
-
end
-
-
66
def contributors = [author, *collaborators, *signers].tap(&:uniq!).sort_by! { it.name.to_s }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Models
-
# Represents a hyperlink.
-
1
Link = Data.define :id, :uri do
-
1
def initialize id: nil, uri: nil
-
546
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Models
-
# Represents a Git tag comprised of multiple commits.
-
1
Tag = Struct.new :author, :commits, :committed_at, :contributors, :sha, :signature, :version
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Models
-
# Represents an external user.
-
1
User = Data.define :external_id, :handle, :name do
-
1
def initialize external_id: nil, handle: nil, name: nil
-
1254
super
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "asciidoctor"
-
-
1
module Milestoner
-
1
module Renderers
-
# Renders ASCII Doc as HTML.
-
1
class Asciidoc
-
1
SETTINGS = {
-
safe: :safe,
-
attributes: {
-
"source-highlighter" => "rouge",
-
"rouge-linenums-mode" => "inline"
-
}
-
}.freeze
-
-
1
def initialize settings: SETTINGS, client: Asciidoctor
-
3
@settings = settings
-
3
@client = client
-
end
-
-
1
def call(content) = client.convert content, settings
-
-
1
private
-
-
1
attr_reader :settings, :client
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "redcarpet"
-
1
require "refinements/module"
-
1
require "rouge"
-
1
require "rouge/plugins/redcarpet"
-
-
1
module Milestoner
-
1
module Renderers
-
# Renders Markdown as HTML.
-
1
class Markdown
-
1
using Refinements::Module
-
-
1
CLIENT = Redcarpet::Markdown.new Class.new(Redcarpet::Render::HTML)
-
.include(Rouge::Plugins::Redcarpet)
-
.pseudonym("redcarpet_html_rouge")
-
.new,
-
disable_indented_code_blocks: true,
-
fenced_code_blocks: true,
-
footnotes: true,
-
highlight: true,
-
superscript: true,
-
tables: true
-
-
1
def initialize client: CLIENT
-
7
@client = client
-
end
-
-
1
def call(content) = client.render content
-
-
1
private
-
-
1
attr_reader :client
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/binding"
-
-
1
module Milestoner
-
1
module Renderers
-
# The primary renderer for multiple input formats as HTML.
-
1
class Universal
-
1
include Dependencies[:settings]
-
-
1
using Refinements::Binding
-
-
1
DELEGATES = {asciidoc: Asciidoc.new, markdown: Markdown.new}.freeze
-
-
1
def initialize(delegates: DELEGATES, **)
-
9
super(**)
-
9
@delegates = delegates
-
9
@default_format = settings.commit_format.to_sym
-
end
-
-
1
def call(content, for: default_format) = delegates.fetch(binding[:for]).call content
-
-
1
private
-
-
1
attr_reader :delegates, :default_format
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "refinements/array"
-
1
require "sanitize"
-
-
1
module Milestoner
-
# A custom HTML sanitizer.
-
1
class Sanitizer
-
1
using Refinements::Array
-
-
1
def initialize defaults: Sanitize::Config::BASIC, client: Sanitize
-
171
@defaults = defaults
-
171
@client = client
-
end
-
-
1
def call(content) = client.fragment content, configuration
-
-
1
private
-
-
1
attr_reader :defaults, :client
-
-
1
def configuration = client::Config.merge(defaults, elements:, attributes:)
-
-
1
def elements
-
214
defaults[:elements].including "audio", "details", "img", "source", "span", "summary", "video"
-
end
-
-
1
def attributes
-
214
defaults[:attributes].merge(
-
all: %w[class id],
-
"a" => %w[href title],
-
"audio" => %w[autoplay controls controlslist crossorigin loop muted preload src],
-
"details" => %w[name open],
-
"img" => %w[alt height loading src width],
-
"source" => %w[type src srcset sizes media height width],
-
"video" => %w[controls height poster src width]
-
)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
module Milestoner
-
1
module Tags
-
# Builds tag message.
-
1
class Builder
-
1
include Milestoner::Dependencies[:settings, :logger]
-
-
1
def initialize(enricher: Enricher.new, view: Views::Milestones::Show.new, **)
-
19
super(**)
-
19
@enricher = enricher
-
19
@view = view
-
end
-
-
1
def call version
-
10
force_minimum
-
-
10
enricher.call
-
8
.fmap { |tags| render tags.first, version }
-
2
.alt_map { |message| failure message }
-
end
-
-
1
private
-
-
1
attr_reader :enricher, :view
-
-
1
def force_minimum = settings.build_max = 1
-
-
1
def render tag, version
-
8
tag.version = version
-
8
view.call(tag:, layout: settings.build_layout, format: :git).to_s.lstrip
-
end
-
-
1
def failure message
-
4
logger.error { message }
-
2
message
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Tags
-
# Handles the creation of project repository tags.
-
1
class Creator
-
1
include Dependencies[:settings, :git, :logger]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(collector: Commits::Collector.new, builder: Builder.new, **)
-
15
super(**)
-
15
@collector = collector
-
15
@builder = builder
-
end
-
-
1
def call version
-
9
then: 2
else: 7
return Success version if local? version
-
-
13
collect.bind { create version }
-
end
-
-
1
private
-
-
1
attr_reader :collector, :builder
-
-
1
def local? version
-
9
then: 2
if git.tag_local? version
-
4
logger.warn { "Local tag exists: #{version}. Skipped." }
-
2
true
-
else: 7
else
-
7
false
-
end
-
end
-
-
2
def collect = collector.call.alt_map { |message| message.sub("fatal: y", "Y").sub("\n", ".") }
-
-
1
def create version
-
12
builder.call(version).bind { |body| git.tag_create version, "#{subject}\n\n#{body}\n\n" }
-
end
-
-
1
def subject = format(settings.tag_subject, **settings.to_h)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
1
require "refinements/pathname"
-
1
require "refinements/struct"
-
1
require "versionaire"
-
-
1
module Milestoner
-
1
module Tags
-
# Builds a collection of enriched tags and associated commits.
-
# :reek:TooManyMethods
-
1
class Enricher
-
1
include Milestoner::Dependencies[:git, :settings, :logger]
-
1
include Commits::Enrichers::Dependencies[author_enricher: :author]
-
1
include Dry::Monads[:result]
-
-
1
using Refinements::Pathname
-
1
using Versionaire::Cast
-
1
using Refinements::Struct
-
-
1
def initialize(committer: Commits::Enricher.new, model: Models::Tag, **)
-
32
super(**)
-
32
@committer = committer
-
32
@model = model
-
end
-
-
1
def call
-
48
collect.fmap { |tags| adjust tags }
-
24
.fmap { |references| slice(references).reverse }
-
24
then: 2
else: 22
.bind { |tags| tags.empty? ? Failure("No tags or commits.") : Success(tags) }
-
end
-
-
1
private
-
-
1
attr_reader :committer, :model
-
-
25
then: 16
else: 8
def collect = git.tagged? ? git.tags("--sort=taggerdate") : placeholder_with_commits
-
-
1
def adjust tags
-
24
references = tags.last(settings.build_max).map(&:version)
-
-
24
maybe_append_head references
-
24
maybe_prepend_nil references, tags
-
24
references
-
end
-
-
1
def maybe_append_head references
-
24
then: 20
else: 4
references.append "HEAD" if settings.build_tail == "head"
-
end
-
-
1
def maybe_prepend_nil references, tags
-
24
max = settings.build_max
-
24
tail = settings.build_tail
-
-
24
then: 5
else: 19
references.prepend nil if references.one? || (tail == "tag" && max >= tags.size)
-
end
-
-
1
def slice references
-
24
references.each_cons(2).with_object [] do |(min, max), entries|
-
27
add_enrichment entries, min, max
-
end
-
end
-
-
26
def add_enrichment(all, *) = enrich(*).bind { |entry| all.append entry }
-
-
1
def enrich min, max
-
54
committer.call(min:, max:).bind { |commits| build_record git.tag_show(max), commits }
-
end
-
-
1
def build_record result, commits
-
54
result.fmap { |tag| record_for tag, commits }
-
rescue Versionaire::Error => error
-
2
logger.error error.message
-
2
Failure error
-
end
-
-
1
def record_for tag, commits
-
27
model[
-
author: author(tag),
-
commits:,
-
committed_at: committed_at(tag.committed_at),
-
contributors: contributors(commits),
-
sha: tag.sha,
-
signature: tag.signature,
-
version: Version(tag.version || settings.project_version)
-
]
-
end
-
-
9
def placeholder_with_commits = committer.call.fmap { |commits| placeholder_for commits }
-
-
1
def placeholder_for commits
-
8
then: 1
else: 7
return commits if commits.empty?
-
-
[
-
7
model[
-
author: commits.last.author,
-
commits:,
-
committed_at: Time.now,
-
contributors: [],
-
version: settings.project_version
-
]
-
]
-
end
-
-
1
def author tag
-
27
author_enricher.call tag.with(author_name: tag.author_name || settings.project_author)
-
end
-
-
# :reek:UtilityFunction
-
1
def contributors commits
-
86
commits.reduce([]) { |all, commit| all.append(*commit.contributors) }
-
.uniq
-
23
.sort_by { it.name.to_s }
-
end
-
-
28
then: 7
else: 20
def committed_at(at) = at ? Time.at(at.to_i) : settings.loaded_at
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "json"
-
1
require "refinements/hash"
-
1
require "refinements/pathname"
-
-
1
module Milestoner
-
1
module Tags
-
# Manages build manifest.
-
1
class Manifest
-
1
include Dependencies[:settings, :git]
-
-
1
using Refinements::Hash
-
1
using Refinements::Pathname
-
-
1
def initialize(name: "manifest.json", **)
-
20
super(**)
-
20
@name = name
-
end
-
-
1
def build_path = settings.build_output.join name
-
-
1
def diff path = build_path
-
3
git.tags.value_or(Core::EMPTY_ARRAY).then do |tags|
-
3
then: 1
else: 2
return Core::EMPTY_HASH if tags.empty?
-
-
2
content_for(tags).diff read(path)
-
end
-
end
-
-
1
def read(path = build_path) = JSON(path.read, {symbolize_names: true})
-
-
1
def write(path = build_path, **)
-
11
path.make_ancestors.write JSON.pretty_generate(generator.deep_merge(**))
-
end
-
-
1
private
-
-
1
attr_reader :name
-
-
1
def content_for tags
-
2
generator.merge latest: tags.last.version, versions: tags.map(&:version)
-
end
-
-
1
def generator
-
13
{generator: {label: settings.generator_label, version: settings.generator_version.to_s}}
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Tags
-
# Handles the tagging and pushing of a tag to a remote repository.
-
1
class Publisher
-
1
include Dependencies[:logger]
-
1
include Dry::Monads[:result]
-
-
1
def initialize(creator: Tags::Creator.new, pusher: Tags::Pusher.new, **)
-
14
super(**)
-
14
@creator = creator
-
14
@pusher = pusher
-
end
-
-
1
def call version
-
8
creator.call(version)
-
6
.bind { pusher.call version }
-
4
.bind { log_info version }
-
end
-
-
1
private
-
-
1
attr_reader :creator, :pusher
-
-
1
def log_info version
-
8
logger.info { "Published: #{version}" }
-
4
Success version
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "dry/monads"
-
-
1
module Milestoner
-
1
module Tags
-
# Handles publishing of tags to a remote repository.
-
1
class Pusher
-
1
include Dependencies[:git, :logger]
-
1
include Dry::Monads[:result]
-
-
1
def call version
-
10
check_remote_repo(version).bind { check_remote_tag version }
-
3
.bind { push version }
-
end
-
-
1
private
-
-
1
def check_remote_repo version
-
6
then: 4
else: 2
git.origin? ? Success(version) : Failure("Remote repository not configured.")
-
end
-
-
1
def check_remote_tag version
-
4
then: 1
else: 3
git.tag_remote?(version) ? Failure("Remote tag exists: #{version}.") : Success(version)
-
end
-
-
1
def push version
-
3
git.tags_push
-
2
.either proc { debug version },
-
1
proc { Failure "Tags could not be pushed to remote repository." }
-
end
-
-
1
def debug version
-
4
logger.debug { "Local tag pushed: #{version}." }
-
2
Success version
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "forwardable"
-
1
require "hanami/view"
-
1
require "refinements/array"
-
-
1
module Milestoner
-
1
module Views
-
# The view context.
-
1
class Context < Hanami::View::Context
-
1
extend Forwardable
-
-
1
include Dependencies[:settings]
-
-
1
using Refinements::Array
-
-
1
delegate %i[
-
build_stylesheet
-
generator_label
-
generator_uri
-
generator_version
-
organization_label
-
organization_uri
-
project_author
-
project_description
-
project_label
-
project_name
-
project_uri_home
-
project_uri_icon
-
project_uri_logo
-
project_uri_version
-
project_version
-
stylesheet_uri
-
] => :settings
-
-
1
def page_title delimiter: " | "
-
42
[project_title, organization_label].compress.join delimiter
-
end
-
-
1
def project_slug
-
4
[project_name, project_version].compact.join("_").tr ".", Core::EMPTY_STRING
-
end
-
-
1
def project_title = [project_label, project_version].compact.join " "
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Milestoner
-
1
module Views
-
1
module Milestones
-
# The index view.
-
1
class Index < Hanami::View
-
1
config.default_context = Context.new
-
1
config.part_namespace = Parts
-
1
config.paths = Container[:settings].build_template_paths
-
1
config.scope_namespace = Scopes
-
1
config.template = "milestones/index"
-
-
1
expose :tags
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Milestoner
-
1
module Views
-
1
module Milestones
-
# The show view.
-
1
class Show < Hanami::View
-
1
include Dependencies[:settings]
-
-
1
config.default_context = Context.new
-
1
config.part_namespace = Parts
-
1
config.paths = Container[:settings].build_template_paths
-
1
config.scope_namespace = Scopes
-
1
config.template = "milestones/show"
-
-
1
expose :past, :tag, :future
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
1
require "refinements/array"
-
-
1
module Milestoner
-
1
module Views
-
1
module Parts
-
# The commit presentation logic.
-
1
class Commit < Hanami::View::Part
-
1
include Dependencies[:settings, :sanitizer, :color]
-
-
1
using Refinements::Array
-
-
1
decorate :author, as: :user
-
1
decorate :collaborators, as: :users
-
1
decorate :signers, as: :users
-
-
1
def initialize(**)
-
2369
super
-
2369
@prefixes = settings.commit_categories.pluck :label
-
end
-
-
1
def colored_author(*custom)
-
10
then: 9
else: 1
custom.push :bold, :blue if custom.empty?
-
10
color[author.name, *custom]
-
end
-
-
1
def colored_created_relative_at(*custom)
-
10
then: 9
else: 1
custom.push :bright_purple if custom.empty?
-
10
color[authored_relative_at, *custom]
-
end
-
-
1
def colored_updated_relative_at(*custom)
-
10
then: 9
else: 1
custom.push :cyan if custom.empty?
-
10
color[committed_relative_at, *custom]
-
end
-
-
1
def colored_sha(*custom)
-
10
then: 9
else: 1
custom.push :yellow if custom.empty?
-
10
color[sha[...12], *custom]
-
end
-
-
1
def created_at_human = created_at.strftime "%Y-%m-%d (%A) %I:%M %p %Z"
-
-
1
def created_at_machine = created_at.strftime "%Y-%m-%dT%H:%M:%S%z"
-
-
1
def kind
-
134
then: 131
else: 3
if prefixes.include? prefix then "normal"
-
3
then: 1
else: 2
elsif directive? then "alert"
-
2
else "error"
-
end
-
end
-
-
1
def emoji
-
87
settings.commit_categories
-
139
.find { |category| category.fetch(:label) == prefix }
-
87
then: 85
else: 2
.then { |category| category ? category.fetch(:emoji) : "🔶" }
-
end
-
-
1
def icon
-
105
then: 103
else: 2
if prefixes.include? prefix then String(prefix).downcase
-
2
then: 1
else: 1
elsif directive? then "rebase"
-
1
else "invalid"
-
end
-
end
-
-
1
def milestone_emoji
-
12
when: 1
case milestone
-
1
when: 1
when "major" then "🔴"
-
1
when: 9
when "minor" then "🔵"
-
9
else: 1
when "patch" then "🟢"
-
1
else "⚪️"
-
end
-
end
-
-
1
def safe_body = sanitizer.call(body_html).html_safe
-
-
1
def safe_notes = sanitizer.call(notes_html).html_safe
-
-
1
def total_deletions = format "%d", -deletions
-
-
1
def total_insertions
-
290
then: 142
else: 3
insertions.then { |total| total.positive? ? "+#{total}" : total.to_s }
-
end
-
-
1
def tag
-
174
then: 1
else: 173
return "rebase" if directive?
-
173
else: 172
then: 1
return "invalid" unless prefixes.include? prefix
-
-
172
milestone
-
end
-
-
1
def popover_id = "po-#{sha}"
-
-
59
then: 57
else: 1
def security = signature == "Good" ? "secure" : "insecure"
-
-
11
then: 9
else: 1
def signature_emoji = signature.then { |kind| kind == "Good" ? "🔒" : "🔓" }
-
-
3
then: 1
else: 1
def signature_id = value.fingerprint.then { |text| text.empty? ? "N/A" : text }
-
-
3
then: 1
else: 1
def signature_key = value.fingerprint_key.then { |text| text.empty? ? "N/A" : text }
-
-
1
def signature_label
-
60
then: 29
else: 1
signature.then { |kind| kind == "Good" ? "🔒 #{kind}" : "🔓 #{kind}" }
-
end
-
-
1
def updated_at_human = updated_at.strftime "%Y-%m-%d (%A) %I:%M %p %Z"
-
-
1
def updated_at_machine = updated_at.strftime "%Y-%m-%dT%H:%M:%S%z"
-
-
1
private
-
-
1
attr_reader :prefixes
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
1
require "refinements/array"
-
1
require "refinements/string"
-
-
1
module Milestoner
-
1
module Views
-
1
module Parts
-
# The tag presentation logic.
-
1
class Tag < Hanami::View::Part
-
1
include Dependencies[:settings, :color, :durationer]
-
-
1
using Refinements::Array
-
1
using Refinements::String
-
-
1
decorate :commits
-
1
decorate :author, as: :user
-
1
decorate :contributors, as: :user
-
-
1
def colored_total_deletions(*custom)
-
11
then: 10
else: 1
custom.push :green if custom.empty?
-
11
color[total_deletions, *custom]
-
end
-
-
1
def colored_total_insertions(*custom)
-
11
then: 10
else: 1
custom.push :red if custom.empty?
-
11
color[total_insertions, *custom]
-
end
-
-
1
def commit_count = commits.size
-
-
1
def committed_at fallback: Time.now
-
338
then: 164
else: 5
value.committed_at.then { |at| at ? Time.at(at) : fallback }
-
end
-
-
1
def committed_date = committed_at.strftime "%Y-%m-%d"
-
-
1
def committed_datetime = committed_at.strftime "%Y-%m-%dT%H:%M:%S%z"
-
-
1
def contributor_names = contributors.map(&:name).to_sentence
-
-
1
def deletion_count = commits.sum(&:deletions)
-
-
1
def duration
-
328
then: 11
else: 317
return 0 if commits.empty?
-
-
317
min = commits.min_by(&:created_at)
-
317
max = commits.max_by(&:updated_at)
-
317
(max.updated_at - min.created_at).to_i
-
end
-
-
1
def empty? = commits.empty?
-
-
1
def file_count = commits.sum(&:files_changed)
-
-
1
def index? = settings.build_index
-
-
1
def insertion_count = commits.sum(&:insertions)
-
-
158
then: 145
else: 12
def security = signature ? "🔒 Tag (secure)" : "🔓 Tag (insecure)"
-
-
165
def total_commits = commit_count.then { |total| "#{total} commit".pluralize "s", total }
-
-
165
def total_files = file_count.then { |total| "#{total} file".pluralize "s", total }
-
-
1
def total_deletions
-
332
deletion_count.then { |total| "#{total} deletion".pluralize "s", total }
-
end
-
-
165
then: 11
else: 153
def total_duration = duration.zero? ? "0 seconds" : durationer.call(duration)
-
-
1
def total_insertions
-
332
insertion_count.then { |total| "#{total} insertion".pluralize "s", total }
-
end
-
-
1
def uri = format settings.project_uri_version, id: version
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Milestoner
-
1
module Views
-
1
module Parts
-
# The user presentation logic.
-
1
class User < Hanami::View::Part
-
1
include Dependencies[:settings]
-
-
786
def name = value.name.then { |text| text || "Unknown" }
-
-
671
def image_alt = value.name.then { |name| name || "missing" }
-
-
1
def avatar_url
-
670
value.name.then do |name|
-
670
then: 645
else: 25
return format settings.avatar_uri, id: value.external_id if name
-
-
25
"https://alchemists.io/images/projects/milestoner/icons/missing.png"
-
end
-
end
-
-
1
def profile_url
-
746
value.name.then do |name|
-
746
then: 685
else: 61
name ? format(settings.profile_uri, id: value.handle) : "/#unknown"
-
end
-
end
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "hanami/view"
-
-
1
module Milestoner
-
1
module Views
-
1
module Scopes
-
# The content specific behavior for partials.
-
1
class Content < Hanami::View::Scope
-
1
def content = String locals.fetch(:content, Core::EMPTY_STRING)
-
-
290
then: 26
else: 263
def call = content.empty? ? render("milestones/none") : render("milestones/content")
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "core"
-
1
require "hanami/view"
-
-
1
module Milestoner
-
1
module Views
-
1
module Scopes
-
# The logo specific behavior for partials.
-
1
class Logo < Hanami::View::Scope
-
184
then: 8
else: 175
def call = project_uri_logo ? render("milestones/logo") : Core::EMPTY_STRING
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Milestoner
-
1
module Views
-
1
module Scopes
-
# The tag signature specific behavior for partials.
-
1
class TagSignature < Hanami::View::Scope
-
1
def initialize(part: Parts::Tag.new(value: Models::Tag.new), **)
-
32
super(**)
-
32
@part = part
-
end
-
-
1
def tag = locals.fetch :tag, part
-
-
1
def call
-
30
then: 28
else: 2
tag.signature ? render("milestones/tag-secure") : render("milestones/tag-insecure")
-
end
-
-
1
private
-
-
1
attr_reader :part
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
require "hanami/view"
-
-
1
module Milestoner
-
1
module Views
-
1
module Scopes
-
# The users specific behavior for partials.
-
1
class Users < Hanami::View::Scope
-
289
then: 247
else: 41
def call = users.any? ? render("milestones/users", users:) : render("milestones/none")
-
end
-
end
-
end
-
end