gel 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 +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/exe/gel +13 -0
- data/lib/gel.rb +22 -0
- data/lib/gel/catalog.rb +153 -0
- data/lib/gel/catalog/common.rb +82 -0
- data/lib/gel/catalog/compact_index.rb +152 -0
- data/lib/gel/catalog/dependency_index.rb +125 -0
- data/lib/gel/catalog/legacy_index.rb +157 -0
- data/lib/gel/catalog/marshal_hacks.rb +16 -0
- data/lib/gel/command.rb +86 -0
- data/lib/gel/command/config.rb +11 -0
- data/lib/gel/command/env.rb +7 -0
- data/lib/gel/command/exec.rb +66 -0
- data/lib/gel/command/help.rb +7 -0
- data/lib/gel/command/install.rb +7 -0
- data/lib/gel/command/install_gem.rb +16 -0
- data/lib/gel/command/lock.rb +34 -0
- data/lib/gel/command/ruby.rb +10 -0
- data/lib/gel/command/shell_setup.rb +25 -0
- data/lib/gel/command/stub.rb +12 -0
- data/lib/gel/command/update.rb +11 -0
- data/lib/gel/compatibility.rb +4 -0
- data/lib/gel/compatibility/bundler.rb +54 -0
- data/lib/gel/compatibility/bundler/cli.rb +6 -0
- data/lib/gel/compatibility/bundler/friendly_errors.rb +3 -0
- data/lib/gel/compatibility/bundler/setup.rb +4 -0
- data/lib/gel/compatibility/rubygems.rb +192 -0
- data/lib/gel/compatibility/rubygems/command.rb +4 -0
- data/lib/gel/compatibility/rubygems/dependency_installer.rb +0 -0
- data/lib/gel/compatibility/rubygems/gem_runner.rb +6 -0
- data/lib/gel/config.rb +80 -0
- data/lib/gel/db.rb +294 -0
- data/lib/gel/direct_gem.rb +29 -0
- data/lib/gel/environment.rb +592 -0
- data/lib/gel/error.rb +104 -0
- data/lib/gel/gemfile_parser.rb +144 -0
- data/lib/gel/gemspec_parser.rb +95 -0
- data/lib/gel/git_catalog.rb +38 -0
- data/lib/gel/git_depot.rb +119 -0
- data/lib/gel/httpool.rb +148 -0
- data/lib/gel/installer.rb +251 -0
- data/lib/gel/lock_loader.rb +164 -0
- data/lib/gel/lock_parser.rb +64 -0
- data/lib/gel/locked_store.rb +126 -0
- data/lib/gel/multi_store.rb +96 -0
- data/lib/gel/package.rb +156 -0
- data/lib/gel/package/inspector.rb +23 -0
- data/lib/gel/package/installer.rb +267 -0
- data/lib/gel/path_catalog.rb +44 -0
- data/lib/gel/pinboard.rb +140 -0
- data/lib/gel/pub_grub/preference_strategy.rb +82 -0
- data/lib/gel/pub_grub/source.rb +153 -0
- data/lib/gel/runtime.rb +27 -0
- data/lib/gel/store.rb +205 -0
- data/lib/gel/store_catalog.rb +31 -0
- data/lib/gel/store_gem.rb +80 -0
- data/lib/gel/stub_set.rb +51 -0
- data/lib/gel/support/gem_platform.rb +225 -0
- data/lib/gel/support/gem_requirement.rb +264 -0
- data/lib/gel/support/gem_version.rb +398 -0
- data/lib/gel/support/tar.rb +13 -0
- data/lib/gel/support/tar/tar_header.rb +229 -0
- data/lib/gel/support/tar/tar_reader.rb +123 -0
- data/lib/gel/support/tar/tar_reader/entry.rb +154 -0
- data/lib/gel/support/tar/tar_writer.rb +339 -0
- data/lib/gel/tail_file.rb +205 -0
- data/lib/gel/version.rb +5 -0
- data/lib/gel/work_pool.rb +143 -0
- data/man/man1/gel-exec.1 +16 -0
- data/man/man1/gel-install.1 +16 -0
- data/man/man1/gel.1 +30 -0
- metadata +131 -0
data/lib/gel/error.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gel::ReportableError
|
4
|
+
# Include this module into any exception that should be treated as a
|
5
|
+
# "user facing error" -- that means an error that's user _reportable_,
|
6
|
+
# not necessarily that it's their fault.
|
7
|
+
#
|
8
|
+
# Examples of things that are user-facing errors:
|
9
|
+
# * unknown subcommand / arguments
|
10
|
+
# * requesting an unknown gem, or one with a malformed name
|
11
|
+
# * problems talking to a gem source
|
12
|
+
# * syntax errors inside a Gemfile or Gemfile.lock
|
13
|
+
# * dependency resolution failure
|
14
|
+
# * problems compiling or installing a gem
|
15
|
+
#
|
16
|
+
# In general, anything that's expected to possibly go wrong should at
|
17
|
+
# some point be wrapped in a user error describing the problem in
|
18
|
+
# terms of what they wanted to do. An unwrapped exception reaching the
|
19
|
+
# top in Command#run is a bug: either the exception is itself
|
20
|
+
# reporting a bug (nil reference, typo on a method name, etc), or if
|
21
|
+
# it's a legitimate failure, then the bug is a missing rescue.
|
22
|
+
|
23
|
+
def details
|
24
|
+
end
|
25
|
+
|
26
|
+
def exit_code
|
27
|
+
1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Base class for user-facing errors. Errors _can_ directly include
|
32
|
+
# ReportableError to bypass this, but absent a specific reason, they
|
33
|
+
# should subclass UserError.
|
34
|
+
#
|
35
|
+
# Prefer narrow-purpose error classes that receive context parameters
|
36
|
+
# over raising generic classes with pre-rendered message parameters. The
|
37
|
+
# former can do a better job of fully describing the problem when
|
38
|
+
# producing detailed CLI output, without filling real code with long
|
39
|
+
# message heredocs.
|
40
|
+
#
|
41
|
+
# Define all UserError subclasses in this file. (Non-reportable errors,
|
42
|
+
# which describe errors in interaction between internal components, can
|
43
|
+
# and should be defined whereever they're used.)
|
44
|
+
class Gel::UserError < StandardError
|
45
|
+
include Gel::ReportableError
|
46
|
+
|
47
|
+
def initialize(**context)
|
48
|
+
@context = context
|
49
|
+
|
50
|
+
super message
|
51
|
+
end
|
52
|
+
|
53
|
+
def [](key)
|
54
|
+
@context.fetch(key)
|
55
|
+
end
|
56
|
+
|
57
|
+
def message
|
58
|
+
self.class.name
|
59
|
+
end
|
60
|
+
|
61
|
+
def inner_backtrace
|
62
|
+
return [] unless cause
|
63
|
+
|
64
|
+
bt = cause.backtrace_locations
|
65
|
+
ignored_bt = backtrace_locations
|
66
|
+
|
67
|
+
while bt.last.to_s == ignored_bt.last.to_s
|
68
|
+
bt.pop
|
69
|
+
ignored_bt.pop
|
70
|
+
end
|
71
|
+
|
72
|
+
while bt.last.path == ignored_bt.last.path
|
73
|
+
bt.pop
|
74
|
+
end
|
75
|
+
|
76
|
+
bt
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module Gel::Error
|
81
|
+
class GemfileEvaluationError < Gel::UserError
|
82
|
+
def initialize(filename:)
|
83
|
+
super
|
84
|
+
end
|
85
|
+
|
86
|
+
def message
|
87
|
+
"Failed to evaluate #{self[:filename].inspect}: #{cause&.message}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def details
|
91
|
+
inner_backtrace.join("\n")
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class BrokenStubError < Gel::UserError
|
96
|
+
def initialize(name:)
|
97
|
+
super
|
98
|
+
end
|
99
|
+
|
100
|
+
def message
|
101
|
+
"No available gem supplies a #{self[:name].inspect} executable"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gel::GemfileParser
|
4
|
+
def self.parse(content, filename = nil, lineno = nil)
|
5
|
+
result = GemfileContent.new(filename)
|
6
|
+
context = ParseContext.new(result, filename)
|
7
|
+
if filename
|
8
|
+
context.instance_eval(content, filename, lineno)
|
9
|
+
else
|
10
|
+
context.instance_eval(content)
|
11
|
+
end
|
12
|
+
result
|
13
|
+
rescue ScriptError, StandardError
|
14
|
+
raise Gel::Error::GemfileEvaluationError.new(filename: filename)
|
15
|
+
end
|
16
|
+
|
17
|
+
class ParseContext
|
18
|
+
def initialize(result, filename)
|
19
|
+
@result = result
|
20
|
+
|
21
|
+
@stack = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def source(uri)
|
25
|
+
if block_given?
|
26
|
+
begin
|
27
|
+
@stack << { source: uri }
|
28
|
+
yield
|
29
|
+
ensure
|
30
|
+
@stack.pop
|
31
|
+
end
|
32
|
+
else
|
33
|
+
@result.sources << uri
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def git_source(name, &block)
|
38
|
+
@result.git_sources[name] = block
|
39
|
+
end
|
40
|
+
|
41
|
+
def ruby(version, engine: nil, engine_version: nil)
|
42
|
+
req = Gel::Support::GemRequirement.new(version)
|
43
|
+
raise "Running ruby version #{RUBY_VERSION} does not match requested #{version.inspect}" unless req.satisfied_by?(Gel::Support::GemVersion.new(RUBY_VERSION))
|
44
|
+
raise "Running ruby engine #{RUBY_ENGINE} does not match requested #{engine.inspect}" unless !engine || RUBY_ENGINE == engine
|
45
|
+
if engine_version
|
46
|
+
raise "Cannot specify :engine_version without :engine" unless engine
|
47
|
+
req = Gel::Support::GemRequirement.new(version)
|
48
|
+
raise "Running ruby engine version #{RUBY_ENGINE_VERSION} does not match requested #{engine_version.inspect}" unless req.satisfied_by?(Gel::Support::GemVersion.new(RUBY_ENGINE_VERSION))
|
49
|
+
end
|
50
|
+
@result.ruby << [version, engine: engine, engine_version: engine_version]
|
51
|
+
end
|
52
|
+
|
53
|
+
def gem(name, *requirements, **options)
|
54
|
+
options = @result.flatten(options, @stack)
|
55
|
+
@result.add_gem(name, requirements, options)
|
56
|
+
end
|
57
|
+
|
58
|
+
def gemspec
|
59
|
+
if file = Dir["#{File.dirname(@result.filename)}/*.gemspec"].first
|
60
|
+
spec = Gel::GemspecParser.parse(File.read(file), file)
|
61
|
+
gem spec.name, path: "."
|
62
|
+
spec.development_dependencies.each do |name, constraints|
|
63
|
+
gem name, constraints, group: :development
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def group(*names)
|
69
|
+
@stack << { group: names }
|
70
|
+
yield
|
71
|
+
ensure
|
72
|
+
@stack.pop
|
73
|
+
end
|
74
|
+
|
75
|
+
def platforms(*names)
|
76
|
+
@stack << { platforms: names }
|
77
|
+
yield
|
78
|
+
ensure
|
79
|
+
@stack.pop
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class GemfileContent
|
84
|
+
attr_reader :filename
|
85
|
+
|
86
|
+
attr_reader :sources
|
87
|
+
attr_reader :git_sources
|
88
|
+
attr_reader :ruby
|
89
|
+
|
90
|
+
attr_reader :gems
|
91
|
+
|
92
|
+
def initialize(filename)
|
93
|
+
@filename = filename
|
94
|
+
@sources = []
|
95
|
+
@git_sources = {}
|
96
|
+
@ruby = []
|
97
|
+
@gems = []
|
98
|
+
end
|
99
|
+
|
100
|
+
def flatten(options, stack)
|
101
|
+
options = options.dup
|
102
|
+
stack.reverse_each do |layer|
|
103
|
+
options.update(layer) { |_, current, outer| current }
|
104
|
+
end
|
105
|
+
@git_sources.each do |key, block|
|
106
|
+
next unless options.key?(key)
|
107
|
+
raise "Multiple git sources specified" if options.key?(:git)
|
108
|
+
options[:git] = block.call(options.delete(key))
|
109
|
+
end
|
110
|
+
options
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_gem(name, requirements, options)
|
114
|
+
return if name == "bundler"
|
115
|
+
raise "Only git sources can specify a :branch" if options[:branch] && !options[:git]
|
116
|
+
raise "Duplicate entry for gem #{name.inspect}" if @gems.assoc(name)
|
117
|
+
|
118
|
+
@gems << [name, requirements, options]
|
119
|
+
end
|
120
|
+
|
121
|
+
def autorequire(target, gems = self.gems)
|
122
|
+
gems.each do |name, _version, options|
|
123
|
+
next if options[:require] == false
|
124
|
+
|
125
|
+
if [nil, true].include?(options[:require])
|
126
|
+
alt_name = name.include?("-") && name.tr("-", "/")
|
127
|
+
if target.gem_has_file?(name, name)
|
128
|
+
target.scoped_require name, name
|
129
|
+
elsif alt_name && target.gem_has_file?(name, alt_name)
|
130
|
+
target.scoped_require name, alt_name
|
131
|
+
elsif options[:require] == true
|
132
|
+
target.scoped_require name, name
|
133
|
+
end
|
134
|
+
elsif options[:require].is_a?(Array)
|
135
|
+
options[:require].each do |path|
|
136
|
+
target.scoped_require name, path
|
137
|
+
end
|
138
|
+
else
|
139
|
+
target.scoped_require name, options[:require]
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ostruct"
|
4
|
+
|
5
|
+
class Gel::GemspecParser
|
6
|
+
module Context
|
7
|
+
def self.context
|
8
|
+
binding
|
9
|
+
end
|
10
|
+
|
11
|
+
module Gem
|
12
|
+
Version = Gel::Support::GemVersion
|
13
|
+
Requirement = Gel::Support::GemRequirement
|
14
|
+
|
15
|
+
VERSION = "2.99.0"
|
16
|
+
|
17
|
+
module Platform
|
18
|
+
RUBY = "ruby"
|
19
|
+
end
|
20
|
+
|
21
|
+
module Specification
|
22
|
+
def self.new(name = nil, version = nil, &block)
|
23
|
+
result = Result.new
|
24
|
+
result.name = name
|
25
|
+
result.version = version
|
26
|
+
block.call result
|
27
|
+
result
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Result < OpenStruct
|
34
|
+
def initialize
|
35
|
+
super
|
36
|
+
self.specification_version = nil
|
37
|
+
self.metadata = {}
|
38
|
+
self.requirements = []
|
39
|
+
self.rdoc_options = []
|
40
|
+
self.development_dependencies = []
|
41
|
+
self.runtime_dependencies = []
|
42
|
+
self.executables = []
|
43
|
+
end
|
44
|
+
|
45
|
+
def add_development_dependency(name, *versions)
|
46
|
+
development_dependencies << [name, versions.flatten]
|
47
|
+
end
|
48
|
+
|
49
|
+
def add_runtime_dependency(name, *versions)
|
50
|
+
runtime_dependencies << [name, versions.flatten]
|
51
|
+
end
|
52
|
+
alias add_dependency add_runtime_dependency
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.parse(content, filename, lineno = 1, root: File.dirname(filename), isolate: true)
|
56
|
+
filename = File.expand_path(filename)
|
57
|
+
root = File.expand_path(root)
|
58
|
+
|
59
|
+
if isolate
|
60
|
+
in_read, in_write = IO.pipe
|
61
|
+
out_read, out_write = IO.pipe
|
62
|
+
|
63
|
+
pid = spawn({ "RUBYLIB" => Gel::Environment.modified_rubylib, "GEL_GEMFILE" => "", "GEL_LOCKFILE" => "" },
|
64
|
+
RbConfig.ruby,
|
65
|
+
"-r", File.expand_path("compatibility", __dir__),
|
66
|
+
"-r", File.expand_path("gemspec_parser", __dir__),
|
67
|
+
"-e", "puts Marshal.dump(Gel::GemspecParser.parse($stdin.read, ARGV.shift, ARGV.shift.to_i, root: ARGV.shift, isolate: false))",
|
68
|
+
filename, lineno.to_s, root,
|
69
|
+
in: in_read, out: out_write)
|
70
|
+
|
71
|
+
in_read.close
|
72
|
+
out_write.close
|
73
|
+
|
74
|
+
write_thread = Thread.new do
|
75
|
+
in_write.write content
|
76
|
+
in_write.close
|
77
|
+
end
|
78
|
+
|
79
|
+
read_thread = Thread.new do
|
80
|
+
out_read.read
|
81
|
+
end
|
82
|
+
|
83
|
+
_, status = Process.waitpid2(pid)
|
84
|
+
raise "Gemspec parse failed" unless status.success?
|
85
|
+
|
86
|
+
write_thread.join
|
87
|
+
Marshal.load read_thread.value
|
88
|
+
|
89
|
+
else
|
90
|
+
Dir.chdir(root) do
|
91
|
+
Context.context.eval(content, filename, lineno)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "path_catalog"
|
4
|
+
|
5
|
+
class Gel::GitCatalog
|
6
|
+
attr_reader :git_depot, :remote, :ref_type, :ref
|
7
|
+
|
8
|
+
def initialize(git_depot, remote, ref_type, ref)
|
9
|
+
@git_depot = git_depot
|
10
|
+
@remote = remote
|
11
|
+
@ref_type = ref_type
|
12
|
+
@ref = ref
|
13
|
+
|
14
|
+
@monitor = Monitor.new
|
15
|
+
@result = nil
|
16
|
+
end
|
17
|
+
|
18
|
+
def checkout_result
|
19
|
+
@result ||
|
20
|
+
@monitor.synchronize { @result ||= git_depot.resolve_and_checkout(remote, ref) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def revision
|
24
|
+
checkout_result[0]
|
25
|
+
end
|
26
|
+
|
27
|
+
def gem_info(name)
|
28
|
+
path_catalog.gem_info(name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def path_catalog
|
32
|
+
@path_catalog ||= Gel::PathCatalog.new(checkout_result[1])
|
33
|
+
end
|
34
|
+
|
35
|
+
def prepare
|
36
|
+
checkout_result
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Gel::GitDepot
|
4
|
+
attr_reader :mirror_root
|
5
|
+
|
6
|
+
require "logger"
|
7
|
+
Logger = ::Logger.new($stderr)
|
8
|
+
Logger.level = $DEBUG ? ::Logger::DEBUG : ::Logger::WARN
|
9
|
+
|
10
|
+
def initialize(store, mirror_root = (ENV["GEL_CACHE"] || "~/.cache/gel") + "/git")
|
11
|
+
@store = store
|
12
|
+
@mirror_root = File.expand_path(mirror_root)
|
13
|
+
end
|
14
|
+
|
15
|
+
def git_path(remote, revision)
|
16
|
+
short = File.basename(remote, ".git")
|
17
|
+
File.join(@store.root, "git", "#{short}-#{revision[0..12]}")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns a path containing a local mirror of the given remote.
|
21
|
+
#
|
22
|
+
# If the mirror already exists, yields the path (same as the return
|
23
|
+
# value); if the block returns false the mirror will be updated.
|
24
|
+
def remote(remote)
|
25
|
+
cache_dir = "#{@mirror_root}/#{ident(remote)}"
|
26
|
+
|
27
|
+
if Dir.exist?(cache_dir)
|
28
|
+
if block_given? && !yield(cache_dir)
|
29
|
+
# The block didn't like what it saw; try updating the mirror
|
30
|
+
# from upstream
|
31
|
+
status = git(remote, "remote", "update", chdir: cache_dir)
|
32
|
+
raise "git remote update failed" unless status.success?
|
33
|
+
end
|
34
|
+
else
|
35
|
+
status = git(remote, "clone", "--mirror", remote, cache_dir)
|
36
|
+
raise "git clone --mirror failed" unless status.success?
|
37
|
+
end
|
38
|
+
|
39
|
+
cache_dir
|
40
|
+
end
|
41
|
+
|
42
|
+
def resolve(remote, ref)
|
43
|
+
mirror = remote(remote) { false } # always update mirror
|
44
|
+
|
45
|
+
r, w = IO.pipe
|
46
|
+
status = git(remote, "rev-parse", ref || "HEAD", chdir: mirror, out: w)
|
47
|
+
raise "git rev-parse failed" unless status.success?
|
48
|
+
|
49
|
+
w.close
|
50
|
+
|
51
|
+
r.read.chomp
|
52
|
+
end
|
53
|
+
|
54
|
+
def resolve_and_checkout(remote, ref)
|
55
|
+
revision = resolve(remote, ref)
|
56
|
+
[revision, checkout(remote, revision)]
|
57
|
+
end
|
58
|
+
|
59
|
+
def checkout(remote, revision)
|
60
|
+
destination = git_path(remote, revision)
|
61
|
+
return destination if Dir.exist?(destination)
|
62
|
+
|
63
|
+
mirror = remote(remote) do |cache_dir|
|
64
|
+
# Check whether the revision is already in our mirror
|
65
|
+
status = git(remote, "rev-list", "--quiet", revision, chdir: cache_dir)
|
66
|
+
status.success?
|
67
|
+
end
|
68
|
+
|
69
|
+
status = git(remote, "clone", mirror, destination)
|
70
|
+
raise "git clone --local failed" unless status.success?
|
71
|
+
|
72
|
+
status = git(remote, "checkout", "--detach", "--force", revision, chdir: destination)
|
73
|
+
raise "git checkout failed" unless status.success?
|
74
|
+
|
75
|
+
destination
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def git(remote, *arguments, **kwargs)
|
81
|
+
kwargs[:in] ||= IO::NULL
|
82
|
+
kwargs[:out] ||= IO::NULL
|
83
|
+
kwargs[:err] ||= IO::NULL
|
84
|
+
|
85
|
+
t = Time.now
|
86
|
+
pid = spawn("git", *arguments, **kwargs)
|
87
|
+
logger.debug { "#{remote} [#{pid}] #{command_for_log("git", *arguments)}" }
|
88
|
+
|
89
|
+
_, status = Process.waitpid2(pid)
|
90
|
+
logger.debug { "#{remote} [#{pid}] process exited #{status.exitstatus} (#{status.success? ? "success" : "failure"}) after #{Time.now - t}s" }
|
91
|
+
|
92
|
+
status
|
93
|
+
end
|
94
|
+
|
95
|
+
def ident(remote)
|
96
|
+
short = File.basename(remote, ".git")
|
97
|
+
digest = Digest(:SHA256).hexdigest(remote)[0..12]
|
98
|
+
"#{short}-#{digest}"
|
99
|
+
end
|
100
|
+
|
101
|
+
require "shellwords"
|
102
|
+
def shellword(word)
|
103
|
+
if word =~ /\A[A-Za-z0-9=+\/,.-]+\z/
|
104
|
+
word
|
105
|
+
elsif word =~ /'/
|
106
|
+
"\"#{Shellwords.shellescape(word).gsub(/\\\s/, "\\1")}\""
|
107
|
+
else
|
108
|
+
"'#{word}'"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def command_for_log(*parts)
|
113
|
+
parts.map { |part| shellword(part) }.join(" ")
|
114
|
+
end
|
115
|
+
|
116
|
+
def logger
|
117
|
+
Logger
|
118
|
+
end
|
119
|
+
end
|