gemkeeper 0.3.0 → 0.4.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/.reek.yml +2 -1
- data/CHANGELOG.md +22 -1
- data/lib/gemkeeper/cli/commands/setup.rb +93 -26
- data/lib/gemkeeper/errors.rb +2 -1
- data/lib/gemkeeper/gem_repo_resolver.rb +80 -0
- data/lib/gemkeeper/lockfile_parser.rb +73 -16
- data/lib/gemkeeper/manifest_reader.rb +31 -6
- data/lib/gemkeeper/version.rb +1 -1
- data/lib/gemkeeper.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a97b09f4efb6325d8efdb7f33130bdf0f2e999c647b68838cb0db370d837d5b0
|
|
4
|
+
data.tar.gz: 55269457dce1d236e620b1f6a1f71b017ee724363363af2fb24e63f7bd583ad7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 630727692a8c0ec33456f0a3438a64b1aac25e14f4385803744b9ae992f97ca32ac3a914aead6b3901a102f9a4d7ad53c758cbd9e5a53baa314f9634a07799c5
|
|
7
|
+
data.tar.gz: e50b7fef90457e4a1941d8f96879775226a7b999cb90b579db36f82a98fb3dbd96e4cc367029b431c1d021f44b44c9521bd94ef32060a31ccb89682ed1f6be18
|
data/.reek.yml
CHANGED
|
@@ -17,7 +17,8 @@ detectors:
|
|
|
17
17
|
- Gemkeeper::ConfigurationError
|
|
18
18
|
- Gemkeeper::ConfigFileNotFoundError
|
|
19
19
|
- Gemkeeper::InvalidConfigError
|
|
20
|
-
- Gemkeeper::
|
|
20
|
+
- Gemkeeper::UnresolvableGemError
|
|
21
|
+
- Gemkeeper::ManifestConflictError
|
|
21
22
|
- Gemkeeper::GitError
|
|
22
23
|
- Gemkeeper::CloneError
|
|
23
24
|
- Gemkeeper::CheckoutError
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-05-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `gemkeeper setup` no longer requires a pre-existing manifest.
|
|
8
|
+
When given a Gemfile.lock, it discovers internal gems by source type and builds or updates `~/.config/gemkeeper/manifest.yml` automatically.
|
|
9
|
+
GIT-sourced gems (declared with `git:` in the Gemfile) are added directly — the repo URL comes from the lockfile.
|
|
10
|
+
Gems from private registries (e.g. GitHub Packages) have their repo URL inferred where possible or prompted for interactively; running non-interactively without a resolvable URL exits with a clear error.
|
|
11
|
+
- `gemkeeper setup` accepts either a Gemfile.lock or an existing `gemkeeper.yml` as its argument.
|
|
12
|
+
Passing a `gemkeeper.yml` updates the manifest with its repo mappings and, with `--global`, installs it to the global config path.
|
|
13
|
+
- The `--manifest` option controls the path used for both reading and writing the manifest, defaulting to `~/.config/gemkeeper/manifest.yml`.
|
|
14
|
+
- `gemkeeper setup` now automatically configures Bundler mirror settings for each private gem registry found in the lockfile, pointing them at the local Geminabox.
|
|
15
|
+
Uses `--local` scope by default; `--global` setup uses `--global` scope.
|
|
16
|
+
Pass `--skip-bundler-config` to opt out.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- GIT-sourced gems (pinned in the `GIT` section of the lockfile) are now included in the generated `gemkeeper.yml`.
|
|
21
|
+
Previously they were silently omitted because the lockfile parser only read `GEM` sections.
|
|
22
|
+
|
|
3
23
|
## [0.3.0] - 2026-05-27
|
|
4
24
|
|
|
5
25
|
### Added
|
|
@@ -35,7 +55,8 @@
|
|
|
35
55
|
|
|
36
56
|
- Initial release
|
|
37
57
|
|
|
38
|
-
[Unreleased]: https://github.com/danhorst/gemkeeper/compare/v0.
|
|
58
|
+
[Unreleased]: https://github.com/danhorst/gemkeeper/compare/v0.4.0...HEAD
|
|
59
|
+
[0.4.0]: https://github.com/danhorst/gemkeeper/compare/0.3.0...0.4.0
|
|
39
60
|
[0.3.0]: https://github.com/danhorst/gemkeeper/compare/0.2.1...0.3.0
|
|
40
61
|
[0.2.1]: https://github.com/danhorst/gemkeeper/compare/0.2.0...0.2.1
|
|
41
62
|
[0.2.0]: https://github.com/danhorst/gemkeeper/compare/0.1.0...0.2.0
|
|
@@ -4,10 +4,10 @@ module Gemkeeper
|
|
|
4
4
|
module CLI
|
|
5
5
|
module Commands
|
|
6
6
|
class Setup < Dry::CLI::Command
|
|
7
|
-
desc "Generate gemkeeper.yml from a Gemfile.lock
|
|
7
|
+
desc "Generate gemkeeper.yml from a Gemfile.lock or existing gemkeeper.yml"
|
|
8
8
|
|
|
9
|
-
argument :
|
|
10
|
-
|
|
9
|
+
argument :source_path, type: :string, required: true,
|
|
10
|
+
desc: "Path to a Gemfile.lock or gemkeeper.yml"
|
|
11
11
|
option :manifest, type: :string,
|
|
12
12
|
desc: "Path to gem manifest (default: ~/.config/gemkeeper/manifest.yml)"
|
|
13
13
|
option :config, type: :string, desc: "Path to write gemkeeper.yml (default: ./gemkeeper.yml)"
|
|
@@ -15,27 +15,108 @@ module Gemkeeper
|
|
|
15
15
|
desc: "Write to the global service config (for use with brew services)"
|
|
16
16
|
option :force, type: :boolean, default: false,
|
|
17
17
|
desc: "Overwrite existing gemkeeper.yml entirely"
|
|
18
|
+
option :skip_bundler_config, type: :boolean, default: false,
|
|
19
|
+
desc: "Skip configuring Bundler mirrors for private gem sources"
|
|
18
20
|
|
|
19
|
-
def call(
|
|
21
|
+
def call(source_path:, **options)
|
|
20
22
|
validate_options!(options)
|
|
21
|
-
|
|
22
23
|
output_path = resolve_output_path(options)
|
|
23
|
-
|
|
24
|
+
|
|
25
|
+
if lockfile?(source_path)
|
|
26
|
+
setup_from_lockfile(source_path, output_path, options)
|
|
27
|
+
else
|
|
28
|
+
setup_from_config(source_path, output_path, options)
|
|
29
|
+
end
|
|
30
|
+
rescue UnresolvableGemError, ManifestConflictError => error
|
|
31
|
+
warn "Error: #{error.message}"
|
|
32
|
+
exit 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def lockfile?(path)
|
|
38
|
+
File.extname(path) == ".lock" || File.basename(path) == "Gemfile.lock"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def setup_from_lockfile(lockfile_path, output_path, options)
|
|
42
|
+
manifest = load_manifest(options)
|
|
43
|
+
candidates = LockfileParser.internal_sources(lockfile_path)
|
|
44
|
+
|
|
45
|
+
unless candidates.empty?
|
|
46
|
+
GemRepoResolver.new(candidates:, manifest:).resolve!
|
|
47
|
+
manifest.save(manifest_path(options))
|
|
48
|
+
end
|
|
49
|
+
|
|
24
50
|
lockfile_versions = LockfileParser.parse(lockfile_path)
|
|
25
51
|
global_output_path = options[:global] ? output_path : nil
|
|
26
|
-
|
|
27
52
|
config = ConfigGenerator.new(manifest:, lockfile_versions:)
|
|
28
53
|
.build(output_path, force: options[:force], global_output_path:)
|
|
29
54
|
|
|
30
55
|
File.write(output_path, config.to_yaml)
|
|
31
56
|
puts "Wrote #{output_path}"
|
|
32
|
-
|
|
33
|
-
rescue ManifestNotFoundError => error
|
|
34
|
-
warn "Error: #{error.message}"
|
|
35
|
-
exit 1
|
|
57
|
+
configure_bundler(candidates, config, options) unless options[:skip_bundler_config]
|
|
36
58
|
end
|
|
37
59
|
|
|
38
|
-
|
|
60
|
+
def configure_bundler(candidates, config, options)
|
|
61
|
+
remotes = candidates.filter_map { |c| c[:remote] if c[:source_type] == :private_gem }.uniq
|
|
62
|
+
return if remotes.empty?
|
|
63
|
+
|
|
64
|
+
port = config.fetch("port", Configuration::DEFAULT_PORT)
|
|
65
|
+
local_url = "http://localhost:#{port}"
|
|
66
|
+
scope = options[:global] ? "--global" : "--local"
|
|
67
|
+
|
|
68
|
+
puts ""
|
|
69
|
+
remotes.each do |remote|
|
|
70
|
+
if system("bundle", "config", "set", scope, "mirror.#{remote}", local_url, out: File::NULL)
|
|
71
|
+
puts "Configured: bundle config set #{scope} mirror.#{remote} #{local_url}"
|
|
72
|
+
else
|
|
73
|
+
warn "Warning: failed to configure bundler mirror for #{remote}"
|
|
74
|
+
warn " Run manually: bundle config set #{scope} mirror.#{remote} #{local_url}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def setup_from_config(source_path, output_path, options)
|
|
80
|
+
source = YAML.safe_load_file(source_path, permitted_classes: [], symbolize_names: false) || {}
|
|
81
|
+
manifest = load_manifest(options)
|
|
82
|
+
update_manifest_from_config(source, manifest, options)
|
|
83
|
+
install_global_config(source, output_path) if options[:global]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def update_manifest_from_config(source, manifest, options)
|
|
87
|
+
(source["gems"] || []).each do |entry|
|
|
88
|
+
repo = entry["repo"].to_s
|
|
89
|
+
next if repo.empty?
|
|
90
|
+
|
|
91
|
+
name = File.basename(repo, ".git").sub(/^ruby-/, "")
|
|
92
|
+
manifest.add_mapping(name:, repo:)
|
|
93
|
+
end
|
|
94
|
+
manifest.save(manifest_path(options))
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def install_global_config(source, output_path)
|
|
98
|
+
existing = File.exist?(output_path) ? (YAML.safe_load_file(output_path) || {}) : {}
|
|
99
|
+
merged = existing.merge(source.except("gems")).merge("gems" => merge_gem_lists(existing, source))
|
|
100
|
+
File.write(output_path, merged.to_yaml)
|
|
101
|
+
puts "Wrote #{output_path}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def merge_gem_lists(existing, source)
|
|
105
|
+
existing_gems = existing["gems"] || []
|
|
106
|
+
source_gems = source["gems"] || []
|
|
107
|
+
existing_repos = existing_gems.to_set { |g| g["repo"] }
|
|
108
|
+
merged = existing_gems.dup
|
|
109
|
+
source_gems.each { |g| merged << g unless existing_repos.include?(g["repo"]) }
|
|
110
|
+
merged
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def load_manifest(options)
|
|
114
|
+
ManifestReader.load(manifest_path(options))
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def manifest_path(options)
|
|
118
|
+
options[:manifest] || ManifestReader::DEFAULT_PATH
|
|
119
|
+
end
|
|
39
120
|
|
|
40
121
|
def validate_options!(options)
|
|
41
122
|
return unless options[:global] && options[:config]
|
|
@@ -54,20 +135,6 @@ module Gemkeeper
|
|
|
54
135
|
warn "Error: no writable global config path found — install Homebrew or create ~/.config/gemkeeper/"
|
|
55
136
|
exit 1
|
|
56
137
|
end
|
|
57
|
-
|
|
58
|
-
def print_bundler_instructions(config, manifest)
|
|
59
|
-
port = config.fetch("port", Configuration::DEFAULT_PORT)
|
|
60
|
-
local_url = "http://localhost:#{port}"
|
|
61
|
-
source_url = manifest.source_url
|
|
62
|
-
puts ""
|
|
63
|
-
puts "To point Bundler at your local Geminabox, run:"
|
|
64
|
-
if source_url
|
|
65
|
-
puts " bundle config set --local mirror.#{source_url} #{local_url}"
|
|
66
|
-
else
|
|
67
|
-
puts " bundle config set --local mirror.<your-private-gem-source-url> #{local_url}"
|
|
68
|
-
puts " (Replace <your-private-gem-source-url> with the gem source URL from your Gemfile)"
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
138
|
end
|
|
72
139
|
end
|
|
73
140
|
|
data/lib/gemkeeper/errors.rb
CHANGED
|
@@ -6,7 +6,8 @@ module Gemkeeper
|
|
|
6
6
|
class ConfigurationError < Error; end
|
|
7
7
|
class ConfigFileNotFoundError < ConfigurationError; end
|
|
8
8
|
class InvalidConfigError < ConfigurationError; end
|
|
9
|
-
class
|
|
9
|
+
class UnresolvableGemError < ConfigurationError; end
|
|
10
|
+
class ManifestConflictError < ConfigurationError; end
|
|
10
11
|
|
|
11
12
|
class GitError < Error; end
|
|
12
13
|
class CloneError < GitError; end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Gemkeeper
|
|
4
|
+
# Resolves gem candidates (from lockfile sources) to git repo URLs.
|
|
5
|
+
# GIT-sourced gems are added automatically; private gem registry entries
|
|
6
|
+
# are inferred where possible (GitHub Packages) or prompted interactively.
|
|
7
|
+
class GemRepoResolver
|
|
8
|
+
def initialize(candidates:, manifest:, input: $stdin, output: $stdout)
|
|
9
|
+
@candidates = candidates
|
|
10
|
+
@manifest = manifest
|
|
11
|
+
@input = input
|
|
12
|
+
@output = output
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def resolve!
|
|
16
|
+
unresolvable = []
|
|
17
|
+
|
|
18
|
+
@candidates.each do |candidate|
|
|
19
|
+
name = candidate[:name]
|
|
20
|
+
next if @manifest.repo_for(name)
|
|
21
|
+
|
|
22
|
+
if candidate[:source_type] == :git
|
|
23
|
+
@manifest.add_mapping(name:, repo: candidate[:repo])
|
|
24
|
+
else
|
|
25
|
+
repo = resolve_private_gem(candidate)
|
|
26
|
+
if repo
|
|
27
|
+
@manifest.add_mapping(name:, repo:)
|
|
28
|
+
else
|
|
29
|
+
unresolvable << candidate
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
raise_unresolvable(unresolvable) if unresolvable.any?
|
|
35
|
+
@manifest
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def resolve_private_gem(candidate)
|
|
41
|
+
inferred = infer_repo(candidate)
|
|
42
|
+
|
|
43
|
+
if interactive?
|
|
44
|
+
prompt(candidate, inferred)
|
|
45
|
+
elsif inferred
|
|
46
|
+
warn "Note: auto-inferred repo for #{candidate[:name]}: #{inferred}"
|
|
47
|
+
inferred
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def infer_repo(candidate)
|
|
52
|
+
remote = candidate[:remote].to_s
|
|
53
|
+
if (match = remote.match(%r{rubygems\.pkg\.github\.com/([^/]+)}))
|
|
54
|
+
"git@github.com:#{match[1]}/#{candidate[:name]}.git"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def prompt(candidate, inferred)
|
|
59
|
+
@output.print "\n #{candidate[:name]} (from #{candidate[:remote]})"
|
|
60
|
+
@output.print "\n Repo URL"
|
|
61
|
+
@output.print " [#{inferred}]" if inferred
|
|
62
|
+
@output.print ": "
|
|
63
|
+
input = @input.gets&.strip
|
|
64
|
+
input = inferred if input.nil? || input.empty?
|
|
65
|
+
input
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def interactive?
|
|
69
|
+
@input.respond_to?(:isatty) && @input.isatty
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def raise_unresolvable(candidates)
|
|
73
|
+
lines = candidates.map { |c| " - #{c[:name]} (from #{c[:remote]})" }.join("\n")
|
|
74
|
+
raise UnresolvableGemError,
|
|
75
|
+
"Cannot resolve repo URLs non-interactively for:\n#{lines}\n\n" \
|
|
76
|
+
"Run this command in a terminal to map these interactively, " \
|
|
77
|
+
"or add them to your manifest manually."
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -4,6 +4,7 @@ module Gemkeeper
|
|
|
4
4
|
# Locates the nearest Gemfile.lock by walking up; callers don't need to know the search algorithm.
|
|
5
5
|
class LockfileParser
|
|
6
6
|
LOCKFILE_NAME = "Gemfile.lock"
|
|
7
|
+
VERSION_SECTION_TYPES = %w[GEM GIT].freeze
|
|
7
8
|
|
|
8
9
|
def self.find(start_dir = Dir.pwd)
|
|
9
10
|
dir = File.expand_path(start_dir)
|
|
@@ -23,38 +24,94 @@ module Gemkeeper
|
|
|
23
24
|
new(lockfile_path).gem_versions
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
def self.internal_sources(lockfile_path)
|
|
28
|
+
new(lockfile_path).internal_sources
|
|
29
|
+
end
|
|
30
|
+
|
|
26
31
|
def initialize(lockfile_path)
|
|
27
32
|
@lockfile_path = lockfile_path
|
|
28
33
|
end
|
|
29
34
|
|
|
35
|
+
# Returns name => version for all GEM and GIT section gems (excludes dependency lines).
|
|
30
36
|
def gem_versions
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
sections = parsed_sections
|
|
38
|
+
sections.select { |s| VERSION_SECTION_TYPES.include?(s[:type]) }
|
|
39
|
+
.each_with_object({}) { |section, versions| versions.merge!(versions_from_section(section)) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns an array of hashes describing gems from non-rubygems.org sources.
|
|
43
|
+
# Each hash has :name, :source_type (:git or :private_gem), and either
|
|
44
|
+
# :repo (for :git) or :remote (the private gem registry URL, for :private_gem).
|
|
45
|
+
def internal_sources
|
|
46
|
+
sections = parsed_sections
|
|
47
|
+
extract_git_sources(sections) + extract_private_gem_sources(sections)
|
|
33
48
|
end
|
|
34
49
|
|
|
35
50
|
private
|
|
36
51
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
def parsed_sections
|
|
53
|
+
sections = []
|
|
54
|
+
current = nil
|
|
55
|
+
File.foreach(@lockfile_path) do |line|
|
|
56
|
+
if line.match?(/\A[A-Z]/)
|
|
57
|
+
sections << current if current
|
|
58
|
+
current = { type: line.strip, lines: [] }
|
|
59
|
+
elsif current
|
|
60
|
+
current[:lines] << line
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
sections << current if current
|
|
64
|
+
sections
|
|
65
|
+
end
|
|
40
66
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
67
|
+
def versions_from_section(section)
|
|
68
|
+
in_specs = false
|
|
69
|
+
section[:lines].each_with_object({}) do |line, versions|
|
|
70
|
+
if line.strip == "specs:"
|
|
71
|
+
in_specs = true
|
|
72
|
+
elsif in_specs && (match = line.chomp.match(/\A ([a-zA-Z0-9_-]+) \(([^)]+)\)\z/))
|
|
73
|
+
versions[match[1]] = match[2]
|
|
46
74
|
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def extract_git_sources(sections)
|
|
79
|
+
sections.select { |s| s[:type] == "GIT" }.flat_map do |section|
|
|
80
|
+
remote = section_remote(section)
|
|
81
|
+
specs_from_section(section).map { |name| { name:, repo: remote, source_type: :git } }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
47
84
|
|
|
48
|
-
|
|
49
|
-
|
|
85
|
+
def extract_private_gem_sources(sections)
|
|
86
|
+
sections.select { |s| s[:type] == "GEM" }.flat_map do |section|
|
|
87
|
+
remote = section_remote(section)
|
|
88
|
+
next [] if rubygems_org?(remote)
|
|
50
89
|
|
|
51
|
-
|
|
90
|
+
specs_from_section(section).map { |name| { name:, remote:, source_type: :private_gem } }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
52
93
|
|
|
53
|
-
|
|
54
|
-
|
|
94
|
+
def section_remote(section)
|
|
95
|
+
section[:lines].each do |line|
|
|
96
|
+
stripped = line.strip
|
|
97
|
+
return stripped.delete_prefix("remote:").strip if stripped.start_with?("remote:")
|
|
55
98
|
end
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def specs_from_section(section)
|
|
103
|
+
in_specs = false
|
|
104
|
+
section[:lines].each_with_object([]) do |line, names|
|
|
105
|
+
if line.strip == "specs:"
|
|
106
|
+
in_specs = true
|
|
107
|
+
elsif in_specs && (match = line.match(/\A ([a-zA-Z0-9_-]+) \(/))
|
|
108
|
+
names << match[1]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
56
112
|
|
|
57
|
-
|
|
113
|
+
def rubygems_org?(remote)
|
|
114
|
+
remote.to_s.include?("rubygems.org")
|
|
58
115
|
end
|
|
59
116
|
end
|
|
60
117
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
4
5
|
|
|
5
6
|
module Gemkeeper
|
|
6
7
|
# Decouples manifest format from sync/setup commands that need the eligible gem list.
|
|
@@ -15,9 +16,9 @@ module Gemkeeper
|
|
|
15
16
|
|
|
16
17
|
def initialize(path)
|
|
17
18
|
@path = path
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
parse_manifest
|
|
19
|
+
@gems = []
|
|
20
|
+
@source_url = nil
|
|
21
|
+
parse_manifest if File.exist?(@path)
|
|
21
22
|
end
|
|
22
23
|
|
|
23
24
|
def gem_names
|
|
@@ -28,6 +29,30 @@ module Gemkeeper
|
|
|
28
29
|
@gems.find { |gem_entry| gem_entry[:name] == name }
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
def repo_for(name)
|
|
33
|
+
find_by_name(name)&.fetch(:repo)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Adds a name→repo mapping. Idempotent for identical entries.
|
|
37
|
+
# Raises ManifestConflictError if the name exists with a different repo.
|
|
38
|
+
def add_mapping(name:, repo:)
|
|
39
|
+
existing = find_by_name(name)
|
|
40
|
+
if existing
|
|
41
|
+
raise ManifestConflictError, conflict_message(name, existing[:repo], repo) if existing[:repo] != repo
|
|
42
|
+
else
|
|
43
|
+
@gems << { name:, repo: }
|
|
44
|
+
end
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def save(path = @path)
|
|
49
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
50
|
+
data = {}
|
|
51
|
+
data["source_url"] = @source_url if @source_url
|
|
52
|
+
data["gems"] = @gems.map { |g| { "name" => g[:name], "repo" => g[:repo] } }
|
|
53
|
+
File.write(path, data.to_yaml)
|
|
54
|
+
end
|
|
55
|
+
|
|
31
56
|
private
|
|
32
57
|
|
|
33
58
|
def parse_manifest
|
|
@@ -38,9 +63,9 @@ module Gemkeeper
|
|
|
38
63
|
end
|
|
39
64
|
end
|
|
40
65
|
|
|
41
|
-
def
|
|
42
|
-
"Manifest
|
|
43
|
-
"
|
|
66
|
+
def conflict_message(name, existing_repo, new_repo)
|
|
67
|
+
"Manifest conflict for #{name.inspect}: " \
|
|
68
|
+
"existing repo #{existing_repo.inspect} differs from #{new_repo.inspect}"
|
|
44
69
|
end
|
|
45
70
|
end
|
|
46
71
|
end
|
data/lib/gemkeeper/version.rb
CHANGED
data/lib/gemkeeper.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "gemkeeper/output"
|
|
|
6
6
|
require_relative "gemkeeper/configuration"
|
|
7
7
|
require_relative "gemkeeper/lockfile_parser"
|
|
8
8
|
require_relative "gemkeeper/manifest_reader"
|
|
9
|
+
require_relative "gemkeeper/gem_repo_resolver"
|
|
9
10
|
require_relative "gemkeeper/config_generator"
|
|
10
11
|
require_relative "gemkeeper/git_repository"
|
|
11
12
|
require_relative "gemkeeper/gem_builder"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gemkeeper
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dan Brubaker Horst
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-cli
|
|
@@ -107,6 +107,7 @@ files:
|
|
|
107
107
|
- lib/gemkeeper/configuration.rb
|
|
108
108
|
- lib/gemkeeper/errors.rb
|
|
109
109
|
- lib/gemkeeper/gem_builder.rb
|
|
110
|
+
- lib/gemkeeper/gem_repo_resolver.rb
|
|
110
111
|
- lib/gemkeeper/gem_uploader.rb
|
|
111
112
|
- lib/gemkeeper/git_repository.rb
|
|
112
113
|
- lib/gemkeeper/lockfile_parser.rb
|