veragem 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +230 -34
- data/Gemfile.lock +254 -0
- data/README.md +143 -48
- data/lib/veragem/citation.rb +116 -0
- data/lib/veragem/generator.rb +105 -56
- data/lib/veragem/license.rb +27 -0
- data/lib/veragem/name.rb +110 -48
- data/lib/veragem/options.rb +116 -30
- data/lib/veragem/project.rb +200 -39
- data/lib/veragem/questions.rb +163 -0
- data/lib/veragem/summary.rb +62 -0
- data/lib/veragem/version.rb +2 -1
- data/lib/veragem.rb +12 -3
- data/plugins.rb +4 -0
- data/sig/veragem.rbs +151 -23
- data/template/.gitattributes +3 -0
- data/template/.githooks/pre-commit +2 -3
- data/template/.github/PULL_REQUEST_TEMPLATE.md +2 -1
- data/template/.github/dependabot.yml +4 -8
- data/template/.github/image-registries.sh +4 -3
- data/template/.github/workflows/dependabot-automerge.yml +29 -34
- data/template/.github/workflows/dependency-review.yml +19 -15
- data/template/.github/workflows/image.yml +65 -14
- data/template/.github/workflows/links.yml +35 -0
- data/template/.github/workflows/nix.yml +3 -3
- data/template/.github/workflows/release.yml +135 -22
- data/template/.github/workflows/ruby-ci.yml +31 -9
- data/template/.github/workflows/security.yml +15 -9
- data/template/.github/workflows/style-check-cocogitto.yml +3 -4
- data/template/.github/workflows/style-check-dprint.yml +10 -10
- data/template/.gitignore +5 -0
- data/template/.mutineer.yml +28 -0
- data/template/.rubocop.yml +49 -10
- data/template/.simplecov.erb +10 -0
- data/template/.yard-lint.yml +386 -0
- data/template/.yardopts +2 -0
- data/template/AGENTS.md.erb +76 -38
- data/template/CITATION.cff.erb +50 -0
- data/template/CODE_OF_CONDUCT.md.erb +1 -1
- data/template/CONTRIBUTING.md.erb +107 -48
- data/template/Gemfile +10 -7
- data/template/README.md.erb +12 -12
- data/template/Rakefile +167 -31
- data/template/Steepfile.erb +1 -1
- data/template/bin/setup +2 -0
- data/template/cliff.toml +40 -23
- data/template/dprint.json +1 -6
- data/template/flake.lock +3 -3
- data/template/flake.nix +25 -10
- data/template/lib/%path%/cli.rb.erb +19 -15
- data/template/lib/%path%/version.rb.erb +1 -1
- data/template/lib/%path%.rb.erb +3 -1
- data/template/sig/%path%.rbs.erb +1 -1
- data/template/test/test_%underscored%.rb.erb +4 -3
- data/template/test/test_cli.rb.erb +37 -12
- data/template/test/test_helper.rb.erb +15 -7
- metadata +16 -4
- data/template/.github/workflows/rubocop-autocorrect.yml +0 -114
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Veragem
|
|
4
|
+
# The gem's licence: one of three choices, each its SPDX identifiers.
|
|
5
|
+
module License
|
|
6
|
+
# Each choice's SPDX identifiers: the gemspec's licenses, the LICENSE-* files.
|
|
7
|
+
SPDX = { "mit" => ["MIT"], "apache" => ["Apache-2.0"], "dual" => ["MIT", "Apache-2.0"] }.freeze
|
|
8
|
+
# The choices, as --help, the question and a refusal name them.
|
|
9
|
+
CHOICES = "mit, apache or dual"
|
|
10
|
+
|
|
11
|
+
# The choice +value+ names: a choice, its SPDX expression (its ORs in either order) or the start
|
|
12
|
+
# of one choice alone, in any case (`Apache-2.0`, `apache-2.0 or mit`, `a`).
|
|
13
|
+
# @param value [String] valid UTF-8, the spaces around it stripped, as Project.check passes it
|
|
14
|
+
# @return [String] the choice
|
|
15
|
+
# @raise [Veragem::Error] it names none
|
|
16
|
+
def self.check(value)
|
|
17
|
+
given = value.downcase
|
|
18
|
+
spdx = SPDX.find { |_, ids| ids.map(&:downcase).sort == given.split(" or ").sort }&.first
|
|
19
|
+
return spdx if spdx
|
|
20
|
+
|
|
21
|
+
case SPDX.keys.select { it.start_with?(given) }
|
|
22
|
+
in [choice] then choice
|
|
23
|
+
else raise Error, "invalid license (#{CHOICES}) #{value.inspect}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/veragem/name.rb
CHANGED
|
@@ -8,11 +8,29 @@ module Veragem
|
|
|
8
8
|
# `bundle gem`'s names, lower-case: words of letters and digits joined by `_` (one constant,
|
|
9
9
|
# `my_gem` is MyGem) or `-` (a namespace, `my-gem` is My::Gem), each constant a letter first.
|
|
10
10
|
PATTERN = /\A[a-z][a-z0-9]*(?:_[a-z0-9]+)*(?:-[a-z][a-z0-9]*(?:_[a-z0-9]+)*)*\z/
|
|
11
|
+
# Windows' devices, whose names no file there takes: a path's word, of its dashes
|
|
12
|
+
RESERVED = /\A(?:con|prn|aux|nul|com\d|lpt\d)\z/
|
|
11
13
|
|
|
12
14
|
# Its modules, as `bundle gem` names them: `my-gem_tools`' My, GemTools.
|
|
15
|
+
# @param name [String] one PATTERN matches
|
|
16
|
+
# @return [Array<String>] outermost first
|
|
13
17
|
def self.modules(name) = name.split("-").map { |word| word.split("_").map(&:capitalize).join }
|
|
14
18
|
|
|
15
|
-
#
|
|
19
|
+
# The file the gem requires: `my-gem`'s is my/gem.
|
|
20
|
+
# @param name [String] one PATTERN matches
|
|
21
|
+
# @return [String]
|
|
22
|
+
def self.path(name) = name.tr("-", "/")
|
|
23
|
+
|
|
24
|
+
# Its words joined by `_` alone: `my-gem`'s my_gem, its test's file name.
|
|
25
|
+
# @param name [String] one PATTERN matches
|
|
26
|
+
# @return [String]
|
|
27
|
+
def self.underscored(name) = name.tr("-", "_")
|
|
28
|
+
|
|
29
|
+
# +name+, or an Error saying why a gem cannot have it, as written: Generator.check adds what its
|
|
30
|
+
# modules and its kind decide.
|
|
31
|
+
# @param name [String] a gem's name, as typed
|
|
32
|
+
# @return [String] +name+
|
|
33
|
+
# @raise [Veragem::Error] a gem cannot have it
|
|
16
34
|
def self.check(name)
|
|
17
35
|
unless PATTERN.match?(name)
|
|
18
36
|
raise Error, "invalid gem name #{name.inspect}: lower-case words of letters and digits, each a " \
|
|
@@ -22,90 +40,134 @@ module Veragem
|
|
|
22
40
|
if name.length > 40 || name.count("-") > 3
|
|
23
41
|
raise Error, "invalid gem name #{name}: at most 40 characters and 4 modules deep"
|
|
24
42
|
end
|
|
25
|
-
# lib/test_helper.rb would shadow the tests' own (Minitest's -Ilib:test)
|
|
26
|
-
raise Error, "invalid gem name #{name}: its lib/test_helper.rb would shadow the tests'" if name == "test_helper"
|
|
27
43
|
|
|
28
|
-
|
|
44
|
+
check_paths(name)
|
|
29
45
|
name
|
|
30
46
|
end
|
|
31
47
|
|
|
32
|
-
# The gem writes lib/<path>.rb, `module` for each of its modules, and tests that name the first
|
|
48
|
+
# The gem writes lib/<path>.rb, `module` for each of its modules, and tests that name the first:
|
|
49
|
+
# none may meet what its tools load.
|
|
50
|
+
# @param name [String] one Name.check takes
|
|
51
|
+
# @return [void]
|
|
52
|
+
# @raise [Veragem::Error] a module or its file would meet another, or they could not be checked
|
|
33
53
|
def self.check_modules(name)
|
|
34
54
|
nestings = nestings_of(name)
|
|
35
|
-
|
|
36
|
-
check_shadowing(name,
|
|
37
|
-
check_defined(name, nestings.zip(
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Its lib/<path>.rb, first on the load path, must not shadow a library its tools require
|
|
41
|
-
# (lib/json.rb, JSON's); its tests' classes, Minitest::Test's, and its gemspec, which `gem build`
|
|
42
|
-
# evaluates in Gem::Specification, must find its module, not one of theirs (`lifecycle_hooks`:
|
|
43
|
-
# Minitest::Test::LifecycleHooks).
|
|
44
|
-
def self.check_shadowing(name, shadowed, found)
|
|
45
|
-
unless shadowed.empty?
|
|
46
|
-
raise Error, "invalid gem name #{name}: its lib/#{name.tr("-", "/")}.rb would shadow #{shadowed}"
|
|
47
|
-
end
|
|
48
|
-
raise Error, "invalid gem name #{name}: #{found} for its module" unless found.empty?
|
|
55
|
+
answers = probe(library_of(name), path(name), nestings)
|
|
56
|
+
check_shadowing(name, answers.fetch(0), answers.fetch(1))
|
|
57
|
+
check_defined(name, nestings.zip(answers.drop(2)).take_while { |_, kind| !kind.to_s.empty? }, nestings.last)
|
|
49
58
|
end
|
|
50
59
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
|
|
55
|
-
|
|
60
|
+
# What the gem's tools load with its code, where installed: Bundler's CLI, which reads its gemspec
|
|
61
|
+
# (bin/setup's `bundle install`); the Rakefile's libraries and its tasks' (RuboCop's, Steep's,
|
|
62
|
+
# YARD's ERB); bin/console's IRB; its tests' (SimpleCov, Pbt).
|
|
63
|
+
LIBRARIES = %w[bundler bundler/cli rake bundler/audit/task csv json minitest/test_task rubocop/rake_task
|
|
64
|
+
steep/rake_task yard rubocop steep erb irb simplecov minitest/autorun pbt].freeze
|
|
65
|
+
# Its files' paths: lib/test_helper.rb would shadow the tests' own (Minitest's -Ilib:test); a
|
|
66
|
+
# device's name, whatever its extension, is no file of Windows' (lib/con.rb), nor git's there.
|
|
67
|
+
private_class_method def self.check_paths(name)
|
|
68
|
+
raise Error, "invalid gem name #{name}: its lib/test_helper.rb would shadow the tests'" if name == "test_helper"
|
|
56
69
|
|
|
57
|
-
|
|
58
|
-
raise Error, "invalid gem name #{name}:
|
|
70
|
+
reserved = name.split("-").grep(RESERVED).first
|
|
71
|
+
raise Error, "invalid gem name #{name}: Windows reserves #{reserved} as a file's name" if reserved
|
|
59
72
|
end
|
|
60
73
|
|
|
61
|
-
# The gem's top-level library: `date-holidays`' date
|
|
62
|
-
def self.library_of(name) = name.split("-").fetch(0)
|
|
63
|
-
|
|
64
|
-
# My, My::Gem
|
|
65
|
-
def self.nestings_of(name) = modules(name).then { |modules| (1..modules.size).map { modules.first(it).join("::") } }
|
|
66
|
-
|
|
67
74
|
# The child: no stdin, a scratch directory, its arguments cleared before the libraries load, their
|
|
68
|
-
# own output kept from the answers, no exit hooks.
|
|
69
|
-
|
|
75
|
+
# own output kept from the answers, no exit hooks (minitest/autorun's). A file shadowed in any
|
|
76
|
+
# case: a file system may ignore it (macOS', Windows').
|
|
77
|
+
PROBE = <<~RUBY.freeze
|
|
70
78
|
library, path, *nestings = ARGV
|
|
71
79
|
first = nestings.fetch(0)
|
|
72
80
|
ARGV.clear
|
|
73
81
|
answers = $stdout.dup
|
|
74
82
|
$stdout.reopen($stderr)
|
|
75
|
-
[
|
|
83
|
+
[*#{LIBRARIES.inspect}, library].each do |feature|
|
|
76
84
|
require feature
|
|
77
85
|
rescue StandardError, ScriptError
|
|
78
86
|
nil
|
|
79
87
|
end
|
|
80
|
-
|
|
88
|
+
files = "\#{path.gsub(/[a-z]/) { "[\#{it}\#{it.upcase}]" }}.{rb,\#{RbConfig::CONFIG["DLEXT"]}}"
|
|
89
|
+
shadowed = $LOAD_PATH.resolve_feature_path(path)&.last ||
|
|
90
|
+
$LOAD_PATH.lazy.flat_map { |dir| Dir.glob(files, base: dir).map { File.join(dir, it) } }
|
|
91
|
+
.first
|
|
92
|
+
answers.puts(shadowed)
|
|
81
93
|
readers = { "its tests" => defined?(Minitest::Test) && Minitest::Test, "gem build" => Gem::Specification }
|
|
82
94
|
found = readers.filter_map do |reader, context|
|
|
83
95
|
owner = context && (context.ancestors - Object.ancestors).find { it.const_defined?(first, false) }
|
|
84
96
|
"\#{reader} would find \#{owner}::\#{first}" if owner
|
|
85
97
|
end
|
|
86
98
|
answers.puts(found.first)
|
|
87
|
-
nestings.each
|
|
99
|
+
nestings.each do |nesting|
|
|
100
|
+
answers.puts(Object.const_defined?(nesting, false) ? Object.const_get(nesting, false).class : "")
|
|
101
|
+
rescue TypeError # an outer one no module (`Noisy = 1`): nothing within it
|
|
102
|
+
answers.puts("")
|
|
103
|
+
end
|
|
88
104
|
answers.flush
|
|
89
105
|
exit!(0)
|
|
90
106
|
RUBY
|
|
91
|
-
private_constant :PROBE
|
|
107
|
+
private_constant :RESERVED, :LIBRARIES, :PROBE
|
|
108
|
+
# Each probe's answers: a question asked once a process
|
|
109
|
+
@probed = {} #: Hash[[String, String, Array[String]], Array[String]]
|
|
92
110
|
|
|
93
111
|
# The file +path+ already requires, what a Minitest::Test or Gem::Specification finds for the
|
|
94
|
-
# first of +nestings+, then each nesting's class
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
112
|
+
# first of +nestings+, then each nesting's class (Module, Class...), each "" when none, where the
|
|
113
|
+
# gem meets them: with what its tools load (LIBRARIES), and its top-level +library+
|
|
114
|
+
# (`date-holidays`: date), which they may require. Asked of a Ruby of its own, the same however
|
|
115
|
+
# this one was started (RUBYOPT's options would print before the answers), and once a process:
|
|
116
|
+
# the answer cannot change. The child answers once the libraries are loaded, then exits: fewer
|
|
117
|
+
# answers are a failure.
|
|
118
|
+
# @param library [String] the gem's top-level library
|
|
119
|
+
# @param path [String] the file the gem requires, lib/'s
|
|
120
|
+
# @param nestings [Array<String>] its modules' full names, outermost first
|
|
121
|
+
# @return [Array<String>] the shadowed file, what is found, then each nesting's class
|
|
122
|
+
# @raise [Veragem::Error] the child did not answer them all, or could not be run
|
|
100
123
|
def self.probe(library, path, nestings)
|
|
124
|
+
@probed[[library, path, nestings]] ||= answers(library, path, nestings)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private_class_method def self.answers(library, path, nestings)
|
|
101
128
|
command = [RbConfig.ruby, "-e", PROBE, library, path, *nestings]
|
|
129
|
+
env = Bundler.unbundled_env.except("RUBYOPT")
|
|
102
130
|
answers = Dir.mktmpdir do |dir|
|
|
103
|
-
IO.popen(
|
|
104
|
-
&:readlines)
|
|
131
|
+
IO.popen(env, command, unsetenv_others: true, in: File::NULL, err: File::NULL, chdir: dir, &:readlines)
|
|
105
132
|
end
|
|
106
|
-
|
|
133
|
+
return answers.map(&:chomp).freeze if answers.size == nestings.size + 2
|
|
134
|
+
|
|
135
|
+
# rubocop:disable-next Style/SpecialGlobalVars -- RBS types $?, and has no English for $CHILD_STATUS
|
|
136
|
+
unchecked(nestings, "their Ruby ended: #{$?}")
|
|
137
|
+
rescue SystemCallError => e
|
|
138
|
+
# a refusal, not a raw Errno, which Bundler would report as its own bug
|
|
139
|
+
unchecked(nestings, e.message)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private_class_method def self.unchecked(nestings, why)
|
|
143
|
+
raise Error, "could not check #{nestings.last} against its tools: #{why}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Its lib/<path>.rb, first on the load path, must not shadow a library its tools may require:
|
|
147
|
+
# this Ruby's (lib/json.rb, JSON's), a gem's they load too; its tests' classes, Minitest::Test's,
|
|
148
|
+
# and its gemspec, which `gem build` evaluates in Gem::Specification, must find its module, not
|
|
149
|
+
# one of theirs (`lifecycle_hooks`: Minitest::Test::LifecycleHooks).
|
|
150
|
+
private_class_method def self.check_shadowing(name, shadowed, found)
|
|
151
|
+
raise Error, "invalid gem name #{name}: its lib/#{path(name)}.rb would shadow #{shadowed}" unless shadowed.empty?
|
|
152
|
+
raise Error, "invalid gem name #{name}: #{found} for its module" unless found.empty?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The +defined+ nestings, outermost first, each with its class: the gem's own module, +innermost+,
|
|
156
|
+
# one defined already (Comparable) would reopen; an outer one that is a class (Set, Logger), it
|
|
157
|
+
# could not open.
|
|
158
|
+
private_class_method def self.check_defined(name, defined, innermost)
|
|
159
|
+
raise Error, "invalid gem name #{name}: #{innermost} is already defined" if defined.last&.first == innermost
|
|
160
|
+
|
|
161
|
+
outer = defined.find { |_, kind| kind != "Module" }
|
|
162
|
+
raise Error, "invalid gem name #{name}: #{outer[0]} is a #{outer[1]}, not a module" if outer
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The gem's top-level library: `date-holidays`' date
|
|
166
|
+
private_class_method def self.library_of(name) = name.split("-").fetch(0)
|
|
107
167
|
|
|
108
|
-
|
|
168
|
+
# Each of its modules' full names, outermost first: `my-gem`'s My, My::Gem.
|
|
169
|
+
private_class_method def self.nestings_of(name)
|
|
170
|
+
modules(name).then { |modules| (1..modules.size).map { modules.first(it).join("::") } }
|
|
109
171
|
end
|
|
110
172
|
end
|
|
111
173
|
end
|
data/lib/veragem/options.rb
CHANGED
|
@@ -1,53 +1,139 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
3
4
|
require "optparse"
|
|
4
5
|
|
|
5
6
|
module Veragem
|
|
6
7
|
# The command line, read into a Project; git's config gives the author, the email and the GitHub
|
|
7
|
-
# user, as `bundle gem` reads them.
|
|
8
|
+
# user, as `bundle gem` reads them. With a terminal for input, what no option gives is asked for.
|
|
8
9
|
module Options
|
|
10
|
+
# The values git's config gives, and their keys there.
|
|
9
11
|
GIT_CONFIG = { author: "user.name", email: "user.email", github: "github.user" }.freeze
|
|
12
|
+
# The values no option and no git config need give.
|
|
13
|
+
DEFAULTS = { summary: "A Ruby gem.", license: "dual" }.freeze
|
|
14
|
+
# --help's first lines.
|
|
15
|
+
BANNER = "Usage: bundle veragem NAME [options]\n" \
|
|
16
|
+
"With a terminal for input, what no option gives (the name too) is asked for."
|
|
17
|
+
# Each option and its --help, in --help's order.
|
|
18
|
+
OPTIONS = [
|
|
19
|
+
["--[no-]exe", "Add a command line, exe/NAME (default: --no-exe, a library alone)"],
|
|
20
|
+
["--license=LICENSE", "#{License::CHOICES} (MIT OR Apache-2.0, the default)"],
|
|
21
|
+
["--author=NAME", "The author (default: git config user.name, else required)"],
|
|
22
|
+
["--[no-]email=EMAIL", "The author's email (default: git config user.email; --no-email, none)"],
|
|
23
|
+
["--github=USER", "The GitHub user or organization (default: git config github.user, else required)"],
|
|
24
|
+
["--summary=TEXT", "The gem's summary (default: A Ruby gem.)"],
|
|
25
|
+
["--[no-]citation", "Add CITATION.cff, the citation GitHub and Zenodo read (default: --no-citation)"],
|
|
26
|
+
["--family-names=NAMES", "With --citation: the author's family names (required)"],
|
|
27
|
+
["--[no-]given-names=NAMES", "With --citation: the author's given names (--no-given-names: none, unasked)"],
|
|
28
|
+
["--[no-]alias=ALIAS", "With --citation: the author's alias, as a handle (--no-alias: none, unasked)"],
|
|
29
|
+
["--orcid=ID", "With --citation: the author's ORCID iD"],
|
|
30
|
+
["--keywords=LIST", "With --citation: keywords, comma-separated"]
|
|
31
|
+
].freeze #: Array[[String, String]]
|
|
32
|
+
private_constant :GIT_CONFIG, :DEFAULTS, :BANNER, :OPTIONS
|
|
10
33
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
34
|
+
# The Project +argv+ gives, what it does not asked of +input+ when a terminal, the questions on
|
|
35
|
+
# +output+ (stderr, as a prompt is): at a terminal the name checked for +dir+ before any question,
|
|
36
|
+
# the generation's check otherwise.
|
|
37
|
+
# @param argv [Array<String>] the command's arguments
|
|
38
|
+
# @param dir [String] where the gem is to be generated
|
|
39
|
+
# @param config [#call] a git config key's value, nil when unset
|
|
40
|
+
# @param input [#gets, #tty?] where the answers are typed; asked only when a terminal
|
|
41
|
+
# @param output [#print, #puts] where the questions are shown
|
|
42
|
+
# @return [Veragem::Project]
|
|
43
|
+
# @raise [Veragem::Error] an argument or a value is refused, or a question has no answer
|
|
44
|
+
def self.parse(argv, dir: Dir.pwd, config: ->(key) { git_config(key) }, input: $stdin, output: $stderr)
|
|
45
|
+
names, given = arguments(argv)
|
|
46
|
+
defaults = { **DEFAULTS, **GIT_CONFIG.transform_values(&config).compact }
|
|
18
47
|
# rubocop:disable-next Lint/NameTypo -- the project index lacks Class#new, and takes Project#nest for it
|
|
19
|
-
Project.new(names
|
|
48
|
+
return Project.new(name_of(names), required(defaults, given)) unless input.tty?
|
|
49
|
+
|
|
50
|
+
ask(names, given, defaults, Questions.new(input, output, dir, git: GIT_CONFIG))
|
|
20
51
|
rescue OptionParser::ParseError => e
|
|
21
|
-
raise Error, "#{e
|
|
52
|
+
raise Error, "#{e} (bundle veragem --help)"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# The names and the options given, each value checked as Project checks it: before the name, and
|
|
56
|
+
# any question.
|
|
57
|
+
private_class_method def self.arguments(argv)
|
|
58
|
+
given = {} #: Hash[Symbol, String | bool]
|
|
59
|
+
# an argument of invalid UTF-8 as bytes: OptionParser's matching raises on it, Project refuses it
|
|
60
|
+
names = parser.permute(argv.map { it.valid_encoding? ? it : it.b }, into: given)
|
|
61
|
+
# OptionParser keys --given-names as :"given-names"; Project's are :given_names
|
|
62
|
+
given.transform_keys! { it.name.tr("-", "_").to_sym }
|
|
63
|
+
# each value as Project checks it (a text's UTF-8 and stripped): the questions compare them with
|
|
64
|
+
# their answers
|
|
65
|
+
checked = given.to_h { |key, value| [key, value.is_a?(String) ? checked(key, value) : value] }
|
|
66
|
+
Citation.check_values(checked, checked[:citation] == true) if checked.key?(:citation)
|
|
67
|
+
[names, checked]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# +value+ as Project checks it; but "-", a question's none, is no option's value, as no answer
|
|
71
|
+
# can give it.
|
|
72
|
+
private_class_method def self.checked(key, value)
|
|
73
|
+
none = Project::OPTIONAL[key]
|
|
74
|
+
if none && Questions.none?(value)
|
|
75
|
+
raise Error, %(invalid #{Project.field(key)} "-": a question's none; for none, #{none})
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
Project.check(key, value)
|
|
22
79
|
end
|
|
23
80
|
|
|
24
|
-
#
|
|
25
|
-
def self.given
|
|
26
|
-
|
|
27
|
-
|
|
81
|
+
# From a terminal: the names counted, as without one, then the name checked before any question.
|
|
82
|
+
private_class_method def self.ask(names, given, defaults, questions)
|
|
83
|
+
# a citation's values given, the citation's question takes y alone: the name is checked for one
|
|
84
|
+
citation = given[:citation] == true || Citation.cited?(given)
|
|
85
|
+
name = questions.name((name_of(names) unless names.empty?), exe: given[:exe] == true, citation:)
|
|
86
|
+
# rubocop:disable-next Lint/NameTypo -- the project index lacks Class#new, and takes Project#nest for it
|
|
87
|
+
Project.new(name, questions.values(name, given, defaults))
|
|
28
88
|
end
|
|
29
89
|
|
|
30
|
-
def self.
|
|
31
|
-
|
|
90
|
+
private_class_method def self.name_of(names)
|
|
91
|
+
return names.fetch(0) if names.size == 1
|
|
92
|
+
raise Error, "no gem name (bundle veragem --help)" if names.empty?
|
|
93
|
+
|
|
94
|
+
raise Error, "one gem name, not #{names.size} (bundle veragem --help)"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The options' values, and git's where none is given: one of git's refused says so, and which
|
|
98
|
+
# options replace it, as a missing author or GitHub user does.
|
|
99
|
+
private_class_method def self.required(defaults, given)
|
|
100
|
+
GIT_CONFIG.except(*given.keys).each do |key, name|
|
|
101
|
+
value = defaults[key] or next
|
|
102
|
+
Project.check(key, value)
|
|
103
|
+
rescue Error => e
|
|
104
|
+
replaced = ["--#{key}", Project::OPTIONAL[key]].compact.join(" or ")
|
|
105
|
+
raise Error, "#{e} (git config #{name}; #{replaced} replaces it)"
|
|
106
|
+
end
|
|
107
|
+
values = defaults.merge(given) #: Hash[Symbol, String | bool | nil]
|
|
108
|
+
check_present(values)
|
|
109
|
+
values #: Project::values
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The author and the GitHub user, which nothing but git defaults.
|
|
113
|
+
private_class_method def self.check_present(values)
|
|
114
|
+
%i[author github].each do |key|
|
|
115
|
+
raise Error, "no #{Project.field(key)}: --#{key}, or git config #{GIT_CONFIG[key]}" unless values[key]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private_class_method def self.parser
|
|
120
|
+
OptionParser.new(BANNER) do |opts|
|
|
32
121
|
opts.program_name = "bundle veragem"
|
|
33
122
|
opts.version = VERSION
|
|
34
|
-
|
|
35
|
-
opts.on("--license=LICENSE", Project::LICENSES.keys, "mit, apache or dual (MIT OR Apache-2.0, the default)")
|
|
36
|
-
opts.on("--author=NAME", "The author (default: git config user.name)")
|
|
37
|
-
opts.on("--email=EMAIL", "The author's email (default: git config user.email)")
|
|
38
|
-
opts.on("--github=USER", "The GitHub user or organization (default: git config github.user)")
|
|
39
|
-
opts.on("--summary=TEXT", "The gem's summary (default: A Ruby gem.)")
|
|
123
|
+
OPTIONS.each { |switch, help| opts.on(switch, help) }
|
|
40
124
|
end
|
|
41
125
|
end
|
|
42
126
|
|
|
43
|
-
# A git config value, nil when unset
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
value =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
127
|
+
# A git config value, nil when unset (status 1): its bytes as UTF-8, whatever the locale, which
|
|
128
|
+
# Project refuses unless valid. A git that cannot run, or cannot read its config, is refused here,
|
|
129
|
+
# with git's reason, before a missing value would be blamed on the config.
|
|
130
|
+
private_class_method def self.git_config(key)
|
|
131
|
+
value, error, status = Open3.capture3("git", "config", "--get", key, binmode: true)
|
|
132
|
+
raise Error, "git config --get #{key}: #{error.strip}" unless status.success? || status.exitstatus == 1
|
|
133
|
+
|
|
134
|
+
value.strip.force_encoding(Encoding::UTF_8).then { it unless it.empty? }
|
|
135
|
+
rescue SystemCallError => e
|
|
136
|
+
raise Error, "cannot run git: #{e}"
|
|
51
137
|
end
|
|
52
138
|
end
|
|
53
139
|
end
|