kettle-dev 1.0.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 +7 -0
- checksums.yaml.gz.sig +2 -0
- data/CHANGELOG.md +47 -0
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +149 -0
- data/LICENSE.txt +21 -0
- data/README.md +661 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/checksums/kettle-dev-1.0.0.gem.sha256 +1 -0
- data/checksums/kettle-dev-1.0.0.gem.sha512 +1 -0
- data/lib/kettle/dev/ci_helpers.rb +171 -0
- data/lib/kettle/dev/tasks.rb +8 -0
- data/lib/kettle/dev/template_helpers.rb +290 -0
- data/lib/kettle/dev/version.rb +12 -0
- data/lib/kettle/dev.rb +108 -0
- data/lib/kettle/emoji_regex.rb +15 -0
- data/lib/kettle-dev.rb +23 -0
- data/sig/kettle/dev/ci_helpers.rbs +29 -0
- data/sig/kettle/dev/template_helpers.rbs +70 -0
- data/sig/kettle/dev.rbs +19 -0
- data.tar.gz.sig +3 -0
- metadata +267 -0
- metadata.gz.sig +0 -0
data/REEK
ADDED
File without changes
|
data/RUBOCOP.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# RuboCop Usage Guide
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
A tale of two RuboCop plugin gems.
|
6
|
+
|
7
|
+
### RuboCop Gradual
|
8
|
+
|
9
|
+
This project uses `rubocop_gradual` instead of vanilla RuboCop for code style checking. The `rubocop_gradual` tool allows for gradual adoption of RuboCop rules by tracking violations in a lock file.
|
10
|
+
|
11
|
+
### RuboCop LTS
|
12
|
+
|
13
|
+
This project uses `rubocop-lts` to ensure, on a best-effort basis, compatibility with Ruby >= 1.9.2.
|
14
|
+
RuboCop rules are meticulously configured by the `rubocop-lts` family of gems to ensure that a project is compatible with a specific version of Ruby. See: https://rubocop-lts.gitlab.io for more.
|
15
|
+
|
16
|
+
## Checking RuboCop Violations
|
17
|
+
|
18
|
+
To check for RuboCop violations in this project, always use:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
bundle exec rake rubocop_gradual:check
|
22
|
+
```
|
23
|
+
|
24
|
+
**Do not use** the standard RuboCop commands like:
|
25
|
+
- `bundle exec rubocop`
|
26
|
+
- `rubocop`
|
27
|
+
|
28
|
+
## Understanding the Lock File
|
29
|
+
|
30
|
+
The `.rubocop_gradual.lock` file tracks all current RuboCop violations in the project. This allows the team to:
|
31
|
+
|
32
|
+
1. Prevent new violations while gradually fixing existing ones
|
33
|
+
2. Track progress on code style improvements
|
34
|
+
3. Ensure CI builds don't fail due to pre-existing violations
|
35
|
+
|
36
|
+
## Common Commands
|
37
|
+
|
38
|
+
- **Check violations**
|
39
|
+
- `bundle exec rake rubocop_gradual`
|
40
|
+
- `bundle exec rake rubocop_gradual:check`
|
41
|
+
- **(Safe) Autocorrect violations, and update lockfile if no new violations**
|
42
|
+
- `bundle exec rake rubocop_gradual:autocorrect`
|
43
|
+
- **Force update the lock file (w/o autocorrect) to match violations present in code**
|
44
|
+
- `bundle exec rake rubocop_gradual:force_update`
|
45
|
+
|
46
|
+
## Workflow
|
47
|
+
|
48
|
+
1. Before submitting a PR, run `bundle exec rake rubocop_gradual:autocorrect`
|
49
|
+
a. or just the default `bundle exec rake`, as autocorrection is a pre-requisite of the default task.
|
50
|
+
2. If there are new violations, either:
|
51
|
+
- Fix them in your code
|
52
|
+
- Run `bundle exec rake rubocop_gradual:force_update` to update the lock file (only for violations you can't fix immediately)
|
53
|
+
3. Commit the updated `.rubocop_gradual.lock` file along with your changes
|
54
|
+
|
55
|
+
## Never add inline RuboCop disables
|
56
|
+
|
57
|
+
Do not add inline `rubocop:disable` / `rubocop:enable` comments anywhere in the codebase (including specs, except when following the few existing `rubocop:disable` patterns for a rule already being disabled elsewhere in the code). We handle exceptions in two supported ways:
|
58
|
+
|
59
|
+
- Permanent/structural exceptions: prefer adjusting the RuboCop configuration (e.g., in `.rubocop.yml`) to exclude a rule for a path or file pattern when it makes sense project-wide.
|
60
|
+
- Temporary exceptions while improving code: record the current violations in `.rubocop_gradual.lock` via the gradual workflow:
|
61
|
+
- `bundle exec rake rubocop_gradual:autocorrect` (preferred; will autocorrect what it can and update the lock only if no new violations were introduced)
|
62
|
+
- If needed, `bundle exec rake rubocop_gradual:force_update` (as a last resort when you cannot fix the newly reported violations immediately)
|
63
|
+
|
64
|
+
In general, treat the rules as guidance to follow; fix violations rather than ignore them. For example, RSpec conventions in this project expect `described_class` to be used in specs that target a specific class under test.
|
65
|
+
|
66
|
+
## Benefits of rubocop_gradual
|
67
|
+
|
68
|
+
- Allows incremental adoption of code style rules
|
69
|
+
- Prevents CI failures due to pre-existing violations
|
70
|
+
- Provides a clear record of code style debt
|
71
|
+
- Enables focused efforts on improving code quality over time
|
data/SECURITY.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# Security Policy
|
2
|
+
|
3
|
+
## Supported Versions
|
4
|
+
|
5
|
+
| Version | Supported |
|
6
|
+
|----------|-----------|
|
7
|
+
| 1.latest | ✅ |
|
8
|
+
|
9
|
+
## Security contact information
|
10
|
+
|
11
|
+
To report a security vulnerability, please use the
|
12
|
+
[Tidelift security contact](https://tidelift.com/security).
|
13
|
+
Tidelift will coordinate the fix and disclosure.
|
14
|
+
|
15
|
+
## Additional Support
|
16
|
+
|
17
|
+
If you are interested in support for versions older than the latest release,
|
18
|
+
please consider sponsoring the project / maintainer @ https://liberapay.com/pboling/donate,
|
19
|
+
or find other sponsorship links in the [README].
|
20
|
+
|
21
|
+
[README]: README.md
|
@@ -0,0 +1 @@
|
|
1
|
+
b4c6725b40f3e0906cd314309dfa6a9f4a8fda0394dacd99f16aa32376275ab9
|
@@ -0,0 +1 @@
|
|
1
|
+
1273b5c26da368293af8c2d0b87efabf5f8af66e89fbeea1a20e26c960dedb130eb1388c151cba85682110cf4fea577680c3a1e7171606b0f913dce7750e5f45
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
require "net/http"
|
5
|
+
require "json"
|
6
|
+
require "uri"
|
7
|
+
|
8
|
+
module Kettle
|
9
|
+
module Dev
|
10
|
+
# CI-related helper functions used by Rake tasks and release tooling.
|
11
|
+
#
|
12
|
+
# This module only exposes module-functions (no instance state) and is
|
13
|
+
# intentionally small so it can be required by both Rake tasks and the
|
14
|
+
# kettle-release executable.
|
15
|
+
module CIHelpers
|
16
|
+
module_function
|
17
|
+
|
18
|
+
# Determine the project root directory.
|
19
|
+
#
|
20
|
+
# Prefers the directory Rake was invoked from (Rake.application.original_dir)
|
21
|
+
# so that tasks shipped with this gem operate relative to the host project.
|
22
|
+
# Falls back to the current working directory when Rake context is absent.
|
23
|
+
#
|
24
|
+
# @return [String] absolute path to the project root
|
25
|
+
def project_root
|
26
|
+
# Too difficult to test every possible branch here, so ignoring
|
27
|
+
# :nocov:
|
28
|
+
dir = if defined?(Rake) && Rake&.application&.respond_to?(:original_dir)
|
29
|
+
Rake.application.original_dir
|
30
|
+
end
|
31
|
+
# :nocov:
|
32
|
+
dir || Dir.pwd
|
33
|
+
end
|
34
|
+
|
35
|
+
# Parse the GitHub owner/repo from the configured origin remote.
|
36
|
+
#
|
37
|
+
# Supports SSH (git@github.com:owner/repo(.git)) and HTTPS
|
38
|
+
# (https://github.com/owner/repo(.git)) forms.
|
39
|
+
#
|
40
|
+
# @return [Array(String, String), nil] [owner, repo] or nil when unavailable
|
41
|
+
def repo_info
|
42
|
+
out, status = Open3.capture2("git", "config", "--get", "remote.origin.url")
|
43
|
+
return unless status.success?
|
44
|
+
url = out.strip
|
45
|
+
if url =~ %r{git@github.com:(.+?)/(.+?)(\.git)?$}
|
46
|
+
[Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
|
47
|
+
elsif url =~ %r{https://github.com/(.+?)/(.+?)(\.git)?$}
|
48
|
+
[Regexp.last_match(1), Regexp.last_match(2).sub(/\.git\z/, "")]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Current git branch name, or nil when not in a repository.
|
53
|
+
# @return [String, nil]
|
54
|
+
def current_branch
|
55
|
+
out, status = Open3.capture2("git", "rev-parse", "--abbrev-ref", "HEAD")
|
56
|
+
status.success? ? out.strip : nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# List workflow YAML basenames under .github/workflows at the given root.
|
60
|
+
#
|
61
|
+
# Excludes maintenance workflows defined by {#exclusions}.
|
62
|
+
#
|
63
|
+
# @param root [String] project root (defaults to {#project_root})
|
64
|
+
# @return [Array<String>] sorted list of basenames (e.g., "ci.yml")
|
65
|
+
def workflows_list(root = project_root)
|
66
|
+
workflows_dir = File.join(root, ".github", "workflows")
|
67
|
+
files = if Dir.exist?(workflows_dir)
|
68
|
+
Dir[File.join(workflows_dir, "*.yml")] + Dir[File.join(workflows_dir, "*.yaml")]
|
69
|
+
else
|
70
|
+
[]
|
71
|
+
end
|
72
|
+
basenames = files.map { |p| File.basename(p) }
|
73
|
+
basenames = basenames.uniq - exclusions
|
74
|
+
basenames.sort
|
75
|
+
end
|
76
|
+
|
77
|
+
# List of workflow files to exclude from interactive menus and checks.
|
78
|
+
#
|
79
|
+
# For reference...
|
80
|
+
#
|
81
|
+
# A list of all worlflows,
|
82
|
+
# with each marked relative to if they exist in this repo,
|
83
|
+
# or at the top of the README marked.
|
84
|
+
#
|
85
|
+
# - ancient (+)
|
86
|
+
# - auto-assign.yml (-)
|
87
|
+
# - codeql-analysis.yml (-)
|
88
|
+
# - coverage.yml (+)
|
89
|
+
# - current.yml (+)
|
90
|
+
# - danger.yml (x)
|
91
|
+
# - dependency-review.yml (-)
|
92
|
+
# - discord-notifier.yml (-)
|
93
|
+
# - heads.yml (+)
|
94
|
+
# - jruby.yml (+)
|
95
|
+
# - legacy.yml (+)
|
96
|
+
# - locked_deps.yml (+)
|
97
|
+
# - opencollective.yml (-)
|
98
|
+
# - style.yml (+)
|
99
|
+
# - supported.yml (+)
|
100
|
+
# - truffle.yml (+)
|
101
|
+
# - unlocked_deps.yml (+)
|
102
|
+
# - unsupported.yml (+)
|
103
|
+
#
|
104
|
+
# All those marked as (-) or (x) are excluded from interactive menus and checks.
|
105
|
+
# The (x) exist because they may be common in other repos.
|
106
|
+
#
|
107
|
+
# @return [Array<String>]
|
108
|
+
def exclusions
|
109
|
+
%w[
|
110
|
+
auto-assign.yml
|
111
|
+
codeql-analysis.yml
|
112
|
+
danger.yml
|
113
|
+
dependency-review.yml
|
114
|
+
discord-notifier.yml
|
115
|
+
opencollective.yml
|
116
|
+
]
|
117
|
+
end
|
118
|
+
|
119
|
+
# Fetch latest workflow run info for a given workflow and branch via GitHub API.
|
120
|
+
#
|
121
|
+
# @param owner [String]
|
122
|
+
# @param repo [String]
|
123
|
+
# @param workflow_file [String] the workflow basename (e.g., "ci.yml")
|
124
|
+
# @param branch [String, nil] branch to query; defaults to {#current_branch}
|
125
|
+
# @param token [String, nil] OAuth token for higher rate limits; defaults to {#default_token}
|
126
|
+
# @return [Hash{String=>String,Integer}, nil] minimal run info or nil on error/none
|
127
|
+
def latest_run(owner:, repo:, workflow_file:, branch: nil, token: default_token)
|
128
|
+
return unless owner && repo
|
129
|
+
b = branch || current_branch
|
130
|
+
return unless b
|
131
|
+
uri = URI("https://api.github.com/repos/#{owner}/#{repo}/actions/workflows/#{workflow_file}/runs?branch=#{URI.encode_www_form_component(b)}&per_page=1")
|
132
|
+
req = Net::HTTP::Get.new(uri)
|
133
|
+
req["User-Agent"] = "kettle-dev/ci-helpers"
|
134
|
+
req["Authorization"] = "token #{token}" if token && !token.empty?
|
135
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
|
136
|
+
return unless res.is_a?(Net::HTTPSuccess)
|
137
|
+
data = JSON.parse(res.body)
|
138
|
+
run = data["workflow_runs"]&.first
|
139
|
+
return unless run
|
140
|
+
{
|
141
|
+
"status" => run["status"],
|
142
|
+
"conclusion" => run["conclusion"],
|
143
|
+
"html_url" => run["html_url"],
|
144
|
+
"id" => run["id"],
|
145
|
+
}
|
146
|
+
rescue StandardError
|
147
|
+
nil
|
148
|
+
end
|
149
|
+
|
150
|
+
# Whether a run has completed successfully.
|
151
|
+
# @param run [Hash, nil]
|
152
|
+
# @return [Boolean]
|
153
|
+
def success?(run)
|
154
|
+
run && run["status"] == "completed" && run["conclusion"] == "success"
|
155
|
+
end
|
156
|
+
|
157
|
+
# Whether a run has completed with a non-success conclusion.
|
158
|
+
# @param run [Hash, nil]
|
159
|
+
# @return [Boolean]
|
160
|
+
def failed?(run)
|
161
|
+
run && run["status"] == "completed" && run["conclusion"] && run["conclusion"] != "success"
|
162
|
+
end
|
163
|
+
|
164
|
+
# Default GitHub token sourced from environment.
|
165
|
+
# @return [String, nil]
|
166
|
+
def default_token
|
167
|
+
ENV["GITHUB_TOKEN"] || ENV["GH_TOKEN"]
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# Load from a "rakelib" directory is automatic!
|
2
|
+
# Adding a custom directory of tasks as a "rakelib" directory makes them available.
|
3
|
+
#
|
4
|
+
# This file is loaded by Kettle::Dev.install_tasks to register the
|
5
|
+
# gem-provided Rake tasks with the host application's Rake context.
|
6
|
+
abs_path = File.expand_path(__dir__)
|
7
|
+
rakelib = "#{abs_path}/rakelib"
|
8
|
+
Rake.add_rakelib(rakelib)
|
@@ -0,0 +1,290 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# :nocov:
|
4
|
+
require "find"
|
5
|
+
require_relative "ci_helpers"
|
6
|
+
|
7
|
+
module Kettle
|
8
|
+
module Dev
|
9
|
+
# Helpers shared by kettle:dev Rake tasks for templating and file ops.
|
10
|
+
module TemplateHelpers
|
11
|
+
# Track results of templating actions across a single process run.
|
12
|
+
# Keys: absolute destination paths (String)
|
13
|
+
# Values: Hash with keys: :action (Symbol, one of :create, :replace, :skip, :dir_create, :dir_replace), :timestamp (Time)
|
14
|
+
@@template_results = {}
|
15
|
+
|
16
|
+
module_function
|
17
|
+
|
18
|
+
# Root of the host project where Rake was invoked
|
19
|
+
# @return [String]
|
20
|
+
def project_root
|
21
|
+
CIHelpers.project_root
|
22
|
+
end
|
23
|
+
|
24
|
+
# Root of this gem's checkout (repository root when working from source)
|
25
|
+
# Calculated relative to lib/kettle/dev/
|
26
|
+
# @return [String]
|
27
|
+
def gem_checkout_root
|
28
|
+
File.expand_path("../../..", __dir__)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Simple yes/no prompt.
|
32
|
+
# @param prompt [String]
|
33
|
+
# @param default [Boolean]
|
34
|
+
# @return [Boolean]
|
35
|
+
def ask(prompt, default)
|
36
|
+
# Force mode: any prompt resolves to Yes when ENV["force"] is set truthy
|
37
|
+
if ENV.fetch("force", "").to_s =~ /\A(1|true|y|yes)\z/i
|
38
|
+
puts "#{prompt} #{default ? "[Y/n]" : "[y/N]"}: Y (forced)"
|
39
|
+
return true
|
40
|
+
end
|
41
|
+
print("#{prompt} #{default ? "[Y/n]" : "[y/N]"}: ")
|
42
|
+
ans = $stdin.gets&.strip
|
43
|
+
ans = "" if ans.nil?
|
44
|
+
if default
|
45
|
+
ans.empty? || ans =~ /\Ay(es)?\z/i
|
46
|
+
else
|
47
|
+
ans =~ /\Ay(es)?\z/i
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Write file content creating directories as needed
|
52
|
+
# @param dest_path [String]
|
53
|
+
# @param content [String]
|
54
|
+
# @return [void]
|
55
|
+
def write_file(dest_path, content)
|
56
|
+
FileUtils.mkdir_p(File.dirname(dest_path))
|
57
|
+
File.open(dest_path, "w") { |f| f.write(content) }
|
58
|
+
end
|
59
|
+
|
60
|
+
# Prefer an .example variant for a given source path when present
|
61
|
+
# For a given intended source path (e.g., "/src/Rakefile"), this will return
|
62
|
+
# "/src/Rakefile.example" if it exists, otherwise returns the original path.
|
63
|
+
# If the given path already ends with .example, it is returned as-is.
|
64
|
+
# @param src_path [String]
|
65
|
+
# @return [String]
|
66
|
+
def prefer_example(src_path)
|
67
|
+
return src_path if src_path.end_with?(".example")
|
68
|
+
example = src_path + ".example"
|
69
|
+
File.exist?(example) ? example : src_path
|
70
|
+
end
|
71
|
+
|
72
|
+
# Record a template action for a destination path
|
73
|
+
# @param dest_path [String]
|
74
|
+
# @param action [Symbol] one of :create, :replace, :skip, :dir_create, :dir_replace
|
75
|
+
# @return [void]
|
76
|
+
def record_template_result(dest_path, action)
|
77
|
+
abs = File.expand_path(dest_path.to_s)
|
78
|
+
if action == :skip && @@template_results.key?(abs)
|
79
|
+
# Preserve the last meaningful action; do not downgrade to :skip
|
80
|
+
return
|
81
|
+
end
|
82
|
+
@@template_results[abs] = {action: action, timestamp: Time.now}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Access all template results (read-only clone)
|
86
|
+
# @return [Hash]
|
87
|
+
def template_results
|
88
|
+
@@template_results.clone
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns true if the given path was created or replaced by the template task in this run
|
92
|
+
# @param dest_path [String]
|
93
|
+
# @return [Boolean]
|
94
|
+
def modified_by_template?(dest_path)
|
95
|
+
rec = @@template_results[File.expand_path(dest_path.to_s)]
|
96
|
+
return false unless rec
|
97
|
+
[:create, :replace, :dir_create, :dir_replace].include?(rec[:action])
|
98
|
+
end
|
99
|
+
|
100
|
+
# Ensure git working tree is clean before making changes in a task.
|
101
|
+
# If not a git repo, this is a no-op.
|
102
|
+
# @param root [String] project root to run git commands in
|
103
|
+
# @param task_label [String] name of the rake task for user-facing messages (e.g., "kettle:dev:install")
|
104
|
+
# @return [void]
|
105
|
+
def ensure_clean_git!(root:, task_label:)
|
106
|
+
inside_repo = begin
|
107
|
+
system("git", "-C", root.to_s, "rev-parse", "--is-inside-work-tree", out: File::NULL, err: File::NULL)
|
108
|
+
rescue StandardError
|
109
|
+
false
|
110
|
+
end
|
111
|
+
return unless inside_repo
|
112
|
+
|
113
|
+
status_output = begin
|
114
|
+
IO.popen(["git", "-C", root.to_s, "status", "--porcelain"], &:read).to_s
|
115
|
+
rescue StandardError
|
116
|
+
""
|
117
|
+
end
|
118
|
+
return if status_output.strip.empty?
|
119
|
+
|
120
|
+
puts "ERROR: Your git working tree has uncommitted changes."
|
121
|
+
puts "#{task_label} may modify files (e.g., .github/, .gitignore, *.gemspec)."
|
122
|
+
puts "Please commit or stash your changes, then re-run: rake #{task_label}"
|
123
|
+
preview = status_output.lines.take(10).map(&:rstrip)
|
124
|
+
unless preview.empty?
|
125
|
+
puts "Detected changes:"
|
126
|
+
preview.each { |l| puts " #{l}" }
|
127
|
+
puts "(showing up to first 10 lines)"
|
128
|
+
end
|
129
|
+
raise Kettle::Dev::Error, "Aborting: git working tree is not clean."
|
130
|
+
end
|
131
|
+
|
132
|
+
# Copy a single file with interactive prompts for create/replace.
|
133
|
+
# Yields content for transformation when block given.
|
134
|
+
# @return [void]
|
135
|
+
def copy_file_with_prompt(src_path, dest_path, allow_create: true, allow_replace: true)
|
136
|
+
return unless File.exist?(src_path)
|
137
|
+
dest_exists = File.exist?(dest_path)
|
138
|
+
action = nil
|
139
|
+
if dest_exists
|
140
|
+
if allow_replace
|
141
|
+
action = ask("Replace #{dest_path}?", true) ? :replace : :skip
|
142
|
+
else
|
143
|
+
puts "Skipping #{dest_path} (replace not allowed)."
|
144
|
+
action = :skip
|
145
|
+
end
|
146
|
+
elsif allow_create
|
147
|
+
action = ask("Create #{dest_path}?", true) ? :create : :skip
|
148
|
+
else
|
149
|
+
puts "Skipping #{dest_path} (create not allowed)."
|
150
|
+
action = :skip
|
151
|
+
end
|
152
|
+
if action == :skip
|
153
|
+
record_template_result(dest_path, :skip)
|
154
|
+
return
|
155
|
+
end
|
156
|
+
|
157
|
+
content = File.read(src_path)
|
158
|
+
content = yield(content) if block_given?
|
159
|
+
write_file(dest_path, content)
|
160
|
+
record_template_result(dest_path, dest_exists ? :replace : :create)
|
161
|
+
puts "Wrote #{dest_path}"
|
162
|
+
end
|
163
|
+
|
164
|
+
# Copy a directory tree, prompting before creating or overwriting.
|
165
|
+
# @return [void]
|
166
|
+
def copy_dir_with_prompt(src_dir, dest_dir)
|
167
|
+
return unless Dir.exist?(src_dir)
|
168
|
+
dest_exists = Dir.exist?(dest_dir)
|
169
|
+
if dest_exists
|
170
|
+
if ask("Replace directory #{dest_dir} (will overwrite files)?", true)
|
171
|
+
Find.find(src_dir) do |path|
|
172
|
+
rel = path.sub(/^#{Regexp.escape(src_dir)}\/?/, "")
|
173
|
+
next if rel.empty?
|
174
|
+
target = File.join(dest_dir, rel)
|
175
|
+
if File.directory?(path)
|
176
|
+
FileUtils.mkdir_p(target)
|
177
|
+
else
|
178
|
+
FileUtils.mkdir_p(File.dirname(target))
|
179
|
+
FileUtils.cp(path, target)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
puts "Updated #{dest_dir}"
|
183
|
+
record_template_result(dest_dir, :dir_replace)
|
184
|
+
else
|
185
|
+
puts "Skipped #{dest_dir}"
|
186
|
+
record_template_result(dest_dir, :skip)
|
187
|
+
end
|
188
|
+
elsif ask("Create directory #{dest_dir}?", true)
|
189
|
+
FileUtils.mkdir_p(dest_dir)
|
190
|
+
Find.find(src_dir) do |path|
|
191
|
+
rel = path.sub(/^#{Regexp.escape(src_dir)}\/?/, "")
|
192
|
+
next if rel.empty?
|
193
|
+
target = File.join(dest_dir, rel)
|
194
|
+
if File.directory?(path)
|
195
|
+
FileUtils.mkdir_p(target)
|
196
|
+
else
|
197
|
+
FileUtils.mkdir_p(File.dirname(target))
|
198
|
+
FileUtils.cp(path, target)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
puts "Created #{dest_dir}"
|
202
|
+
record_template_result(dest_dir, :dir_create)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
# Apply common token replacements used when templating text files
|
207
|
+
# @param content [String]
|
208
|
+
# @param gh_org [String, nil]
|
209
|
+
# @param gem_name [String]
|
210
|
+
# @param namespace [String]
|
211
|
+
# @param namespace_shield [String]
|
212
|
+
# @param gem_shield [String]
|
213
|
+
# @return [String]
|
214
|
+
def apply_common_replacements(content, gh_org:, gem_name:, namespace:, namespace_shield:, gem_shield:)
|
215
|
+
c = content.dup
|
216
|
+
c = c.gsub("kettle-rb", gh_org.to_s) if gh_org && !gh_org.empty?
|
217
|
+
if gem_name && !gem_name.empty?
|
218
|
+
# Replace occurrences of the template gem name in text, including inside
|
219
|
+
# markdown reference labels like [🖼️kettle-dev] and identifiers like kettle-dev-i
|
220
|
+
c = c.gsub("kettle-dev", gem_name)
|
221
|
+
c = c.gsub(/\bKettle::Dev\b/u, namespace) unless namespace.empty?
|
222
|
+
c = c.gsub("Kettle%3A%3ADev", namespace_shield) unless namespace_shield.empty?
|
223
|
+
c = c.gsub("kettle--dev", gem_shield)
|
224
|
+
end
|
225
|
+
c
|
226
|
+
end
|
227
|
+
|
228
|
+
# Parse gemspec metadata and derive useful strings
|
229
|
+
# @param root [String] project root
|
230
|
+
# @return [Hash]
|
231
|
+
def gemspec_metadata(root = project_root)
|
232
|
+
gemspecs = Dir.glob(File.join(root, "*.gemspec"))
|
233
|
+
gemspec_path = gemspecs.first
|
234
|
+
gemspec_text = (gemspec_path && File.file?(gemspec_path)) ? File.read(gemspec_path) : ""
|
235
|
+
gem_name = (gemspec_text[/\bspec\.name\s*=\s*["']([^"']+)["']/, 1] || "").strip
|
236
|
+
min_ruby = (
|
237
|
+
gemspec_text[/\bspec\.minimum_ruby_version\s*=\s*["'](?:>=\s*)?([0-9]+\.[0-9]+(?:\.[0-9]+)?)["']/i, 1] ||
|
238
|
+
gemspec_text[/\bspec\.required_ruby_version\s*=\s*["']>=\s*([0-9]+\.[0-9]+(?:\.[0-9]+)?)["']/i, 1] ||
|
239
|
+
gemspec_text[/\brequired_ruby_version\s*[:=]\s*["'](?:>=\s*)?([0-9]+\.[0-9]+(?:\.[0-9]+)?)["']/i, 1] ||
|
240
|
+
""
|
241
|
+
).strip
|
242
|
+
homepage_line = gemspec_text.lines.find { |l| l =~ /\bspec\.homepage\s*=\s*/ }
|
243
|
+
homepage_val = homepage_line ? homepage_line.split("=", 2).last.to_s.strip : ""
|
244
|
+
if (homepage_val.start_with?("\"") && homepage_val.end_with?("\"")) || (homepage_val.start_with?("'") && homepage_val.end_with?("'"))
|
245
|
+
homepage_val = begin
|
246
|
+
homepage_val[1..-2]
|
247
|
+
rescue
|
248
|
+
homepage_val
|
249
|
+
end
|
250
|
+
end
|
251
|
+
gh_match = homepage_val&.match(%r{github\.com/([^/]+)/([^/]+)}i)
|
252
|
+
gh_org = gh_match && gh_match[1]
|
253
|
+
gh_repo = gh_match && gh_match[2]&.sub(/\.git\z/, "")
|
254
|
+
if gh_org.nil?
|
255
|
+
begin
|
256
|
+
origin_url = IO.popen(["git", "-C", root.to_s, "remote", "get-url", "origin"], &:read).to_s.strip
|
257
|
+
if (m = origin_url.match(%r{github\.com[/:]([^/]+)/([^/]+)}i))
|
258
|
+
gh_org = m[1]
|
259
|
+
gh_repo = m[2]&.sub(/\.git\z/, "")
|
260
|
+
end
|
261
|
+
rescue StandardError
|
262
|
+
# ignore
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
camel = lambda do |s|
|
267
|
+
s.split(/[_-]/).map { |p| p.gsub(/\b([a-z])/) { Regexp.last_match(1).upcase } }.join
|
268
|
+
end
|
269
|
+
namespace = gem_name.to_s.split("-").map { |seg| camel.call(seg) }.join("::")
|
270
|
+
namespace_shield = namespace.gsub("::", "%3A%3A")
|
271
|
+
entrypoint_require = gem_name.to_s.tr("-", "/")
|
272
|
+
gem_shield = gem_name.to_s.gsub("-", "--").gsub("_", "__")
|
273
|
+
|
274
|
+
{
|
275
|
+
gemspec_path: gemspec_path,
|
276
|
+
gem_name: gem_name,
|
277
|
+
min_ruby: min_ruby,
|
278
|
+
homepage: homepage_val,
|
279
|
+
gh_org: gh_org,
|
280
|
+
gh_repo: gh_repo,
|
281
|
+
namespace: namespace,
|
282
|
+
namespace_shield: namespace_shield,
|
283
|
+
entrypoint_require: entrypoint_require,
|
284
|
+
gem_shield: gem_shield,
|
285
|
+
}
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
# :nocov:
|