poepod 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.adoc +47 -10
- data/lib/poepod/cli.rb +80 -27
- data/lib/poepod/file_processor.rb +25 -59
- data/lib/poepod/gem_processor.rb +48 -33
- data/lib/poepod/processor.rb +108 -7
- data/lib/poepod/version.rb +1 -1
- data/poepod.gemspec +1 -1
- data/spec/poepod/cli_spec.rb +49 -13
- data/spec/poepod/file_processor_spec.rb +112 -25
- data/spec/poepod/gem_processor_spec.rb +66 -53
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b205fe50c5830d75893fbfa378b81984bf8a2b73fa8d104509fbbf698e0001fc
|
4
|
+
data.tar.gz: d4e87cef72c92cdcfaadd6bcf5a452bc1a05b13c4927cce10b6c81250ff99c9c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c32f1ffb23fcf86f8cd1625c1deab4dc3599a0baf7b4ad80618f1e37a23f7b8e815b6065d628bf138f0e8ca9c2c849d1a08c31599737bee85711a6b4cd3e88f0
|
7
|
+
data.tar.gz: bdc4de92b51c89cfbeb484a119ce829e8a5c0a7768a032f4ec4299b06f2b35724038b13a2093827fbe839c650c0a18902c544ddd85f842e47ab4e633089edb38
|
data/README.adoc
CHANGED
@@ -42,37 +42,62 @@ Commands:
|
|
42
42
|
poepod wrap GEMSPEC_PATH # Wrap a gem based on its gemspec file
|
43
43
|
----
|
44
44
|
|
45
|
+
=== Global options
|
46
|
+
|
47
|
+
All options can be used for both `wrap` and `concat` commands:
|
48
|
+
|
49
|
+
* `--exclude`: List of patterns to exclude (default: `["node_modules/", ".git/", ".gitignore$", ".DS_Store$", "^\\..+"]`)
|
50
|
+
* `--config`: Path to configuration file
|
51
|
+
* `--include-binary`: Include binary files (encoded in MIME format)
|
52
|
+
* `--include-dot-files`: Include dot files
|
53
|
+
* `--output-file`: Output path
|
54
|
+
* `--base-dir`: Base directory for relative file paths in output
|
55
|
+
* `--include-unstaged`: Include unstaged files from `lib`, `spec`, and `test` directories (for `wrap` command only)
|
56
|
+
|
57
|
+
[source,shell]
|
58
|
+
----
|
59
|
+
$ poepod concat FILES [OUTPUT_FILE] --exclude PATTERNS --config PATH --include-binary --include-dot-files --output-file PATH --base-dir PATH
|
60
|
+
$ poepod wrap GEMSPEC_PATH --exclude PATTERNS --config PATH --include-binary --include-dot-files --output-file PATH --base-dir PATH --include-unstaged
|
61
|
+
----
|
62
|
+
|
45
63
|
=== Concatenating files
|
46
64
|
|
47
65
|
The `concat` command allows you to combine multiple files into a single text
|
48
|
-
file.
|
66
|
+
file.
|
67
|
+
|
68
|
+
This is particularly useful when you want to review or analyze code from
|
49
69
|
multiple files in one place, or when preparing code submissions for AI-powered
|
50
70
|
coding assistants.
|
51
71
|
|
72
|
+
By default, it excludes binary files, dot files, and certain patterns like
|
73
|
+
`node_modules/` and `.git/`.
|
74
|
+
|
52
75
|
[source,shell]
|
53
76
|
----
|
54
77
|
$ poepod concat path/to/files/* output.txt
|
55
78
|
----
|
56
79
|
|
57
|
-
This will concatenate all files from the specified path into
|
80
|
+
This will concatenate all non-binary, non-dot files from the specified path into
|
81
|
+
`output.txt`.
|
58
82
|
|
59
|
-
====
|
83
|
+
==== Including dot files
|
60
84
|
|
61
|
-
|
85
|
+
By default, dot files (hidden files starting with a dot) are excluded.
|
86
|
+
|
87
|
+
To include them, use the `--include-dot-files` option:
|
62
88
|
|
63
89
|
[source,shell]
|
64
90
|
----
|
65
|
-
$ poepod concat path/to/files/* output.txt --
|
91
|
+
$ poepod concat path/to/files/* output.txt --include-dot-files
|
66
92
|
----
|
67
93
|
|
68
|
-
This is helpful when you want to focus on specific parts of your codebase,
|
69
|
-
excluding irrelevant or large directories.
|
70
|
-
|
71
94
|
==== Including binary files
|
72
95
|
|
73
96
|
By default, binary files are excluded to keep the output focused on readable
|
74
|
-
code.
|
75
|
-
|
97
|
+
code.
|
98
|
+
|
99
|
+
To include binary files (encoded in MIME format), use the `--include-binary`
|
100
|
+
option:
|
76
101
|
|
77
102
|
[source,shell]
|
78
103
|
----
|
@@ -82,6 +107,18 @@ $ poepod concat path/to/files/* output.txt --include-binary
|
|
82
107
|
This can be useful when you need to include binary assets or compiled files in
|
83
108
|
your analysis.
|
84
109
|
|
110
|
+
==== Excluding patterns
|
111
|
+
|
112
|
+
You can exclude certain patterns using the `--exclude` option:
|
113
|
+
|
114
|
+
[source,shell]
|
115
|
+
----
|
116
|
+
$ poepod concat path/to/files/* output.txt --exclude node_modules .git build test
|
117
|
+
----
|
118
|
+
|
119
|
+
This is helpful when you want to focus on specific parts of your codebase,
|
120
|
+
excluding irrelevant or large directories.
|
121
|
+
|
85
122
|
=== Wrapping a gem
|
86
123
|
|
87
124
|
The `wrap` command creates a comprehensive snapshot of your gem, including all
|
data/lib/poepod/cli.rb
CHANGED
@@ -6,44 +6,48 @@ require_relative "file_processor"
|
|
6
6
|
require_relative "gem_processor"
|
7
7
|
|
8
8
|
module Poepod
|
9
|
+
# Command-line interface for Poepod
|
9
10
|
class Cli < Thor
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
output_file ||= default_output_file(files.first)
|
22
|
-
output_path = Pathname.new(output_file).expand_path
|
11
|
+
# Define shared options
|
12
|
+
def self.shared_options
|
13
|
+
option :exclude, type: :array, default: Poepod::FileProcessor::EXCLUDE_DEFAULT,
|
14
|
+
desc: "List of patterns to exclude"
|
15
|
+
option :config, type: :string, desc: "Path to configuration file"
|
16
|
+
option :include_binary, type: :boolean, default: false, desc: "Include binary files (encoded in MIME format)"
|
17
|
+
option :include_dot_files, type: :boolean, default: false, desc: "Include dot files"
|
18
|
+
option :output_file, type: :string, desc: "Output path"
|
19
|
+
option :base_dir, type: :string, desc: "Base directory for relative file paths in output"
|
20
|
+
end
|
23
21
|
|
24
|
-
|
25
|
-
|
22
|
+
desc "concat FILES [OUTPUT_FILE]", "Concatenate specified files into one text file"
|
23
|
+
shared_options
|
26
24
|
|
27
|
-
|
28
|
-
|
25
|
+
def concat(*files)
|
26
|
+
check_files(files)
|
27
|
+
output_file = determine_output_file(files)
|
28
|
+
base_dir = options[:base_dir] || Dir.pwd
|
29
|
+
process_files(files, output_file, base_dir)
|
29
30
|
end
|
30
31
|
|
31
32
|
desc "wrap GEMSPEC_PATH", "Wrap a gem based on its gemspec file"
|
33
|
+
shared_options
|
32
34
|
option :include_unstaged, type: :boolean, default: false,
|
33
35
|
desc: "Include unstaged files from lib, spec, and test directories"
|
34
36
|
|
35
37
|
def wrap(gemspec_path)
|
36
|
-
|
38
|
+
base_dir = options[:base_dir] || File.dirname(gemspec_path)
|
39
|
+
processor = Poepod::GemProcessor.new(
|
40
|
+
gemspec_path,
|
41
|
+
include_unstaged: options[:include_unstaged],
|
42
|
+
exclude: options[:exclude],
|
43
|
+
include_binary: options[:include_binary],
|
44
|
+
include_dot_files: options[:include_dot_files],
|
45
|
+
base_dir: base_dir,
|
46
|
+
config_file: options[:config]
|
47
|
+
)
|
37
48
|
success, result, unstaged_files = processor.process
|
38
|
-
|
39
49
|
if success
|
40
|
-
|
41
|
-
if unstaged_files.any?
|
42
|
-
puts "\nWarning: The following files are not staged in git:"
|
43
|
-
puts unstaged_files
|
44
|
-
puts "\nThese files are #{options[:include_unstaged] ? "included" : "not included"} in the wrap."
|
45
|
-
puts "Use --include-unstaged option to include these files." unless options[:include_unstaged]
|
46
|
-
end
|
50
|
+
handle_wrap_result(success, result, unstaged_files)
|
47
51
|
else
|
48
52
|
puts result
|
49
53
|
exit(1)
|
@@ -56,13 +60,62 @@ module Poepod
|
|
56
60
|
|
57
61
|
private
|
58
62
|
|
63
|
+
def check_files(files)
|
64
|
+
return unless files.empty?
|
65
|
+
|
66
|
+
puts "Error: No files specified."
|
67
|
+
exit(1)
|
68
|
+
end
|
69
|
+
|
70
|
+
def determine_output_file(files)
|
71
|
+
options[:output_file] || default_output_file(files.first)
|
72
|
+
end
|
73
|
+
|
74
|
+
def process_files(files, output_file, base_dir)
|
75
|
+
output_path = Pathname.new(output_file).expand_path
|
76
|
+
processor = Poepod::FileProcessor.new(
|
77
|
+
files,
|
78
|
+
output_path,
|
79
|
+
config_file: options[:config],
|
80
|
+
include_binary: options[:include_binary],
|
81
|
+
include_dot_files: options[:include_dot_files],
|
82
|
+
exclude: options[:exclude],
|
83
|
+
base_dir: base_dir
|
84
|
+
)
|
85
|
+
total_files, copied_files = processor.process
|
86
|
+
print_result(total_files, copied_files, output_path)
|
87
|
+
end
|
88
|
+
|
89
|
+
def print_result(total_files, copied_files, output_path)
|
90
|
+
puts "-> #{total_files} files detected."
|
91
|
+
puts "=> #{copied_files} files have been concatenated into #{output_path.relative_path_from(Dir.pwd)}."
|
92
|
+
end
|
93
|
+
|
94
|
+
def handle_wrap_result(success, result, unstaged_files)
|
95
|
+
if success
|
96
|
+
puts "=> The gem has been wrapped into '#{result}'."
|
97
|
+
print_unstaged_files_warning(unstaged_files) if unstaged_files.any?
|
98
|
+
else
|
99
|
+
puts result
|
100
|
+
exit(1)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def print_unstaged_files_warning(unstaged_files)
|
105
|
+
puts "\nWarning: The following files are not staged in git:"
|
106
|
+
puts unstaged_files
|
107
|
+
puts "\nThese files are #{options[:include_unstaged] ? "included" : "not included"} in the wrap."
|
108
|
+
puts "Use --include-unstaged option to include these files." unless options[:include_unstaged]
|
109
|
+
end
|
110
|
+
|
59
111
|
def default_output_file(first_pattern)
|
60
112
|
first_item = Dir.glob(first_pattern).first
|
61
113
|
if first_item
|
62
114
|
if File.directory?(first_item)
|
63
115
|
"#{File.basename(first_item)}.txt"
|
64
116
|
else
|
65
|
-
"#{File.basename(first_item,
|
117
|
+
"#{File.basename(first_item,
|
118
|
+
".*")}_concat.txt"
|
66
119
|
end
|
67
120
|
else
|
68
121
|
"concatenated_output.txt"
|
@@ -1,79 +1,45 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "processor"
|
4
|
-
require "yaml"
|
5
|
-
require "tqdm"
|
6
|
-
require "pathname"
|
7
|
-
require "open3"
|
8
|
-
require "base64"
|
9
|
-
require "mime/types"
|
10
4
|
|
11
5
|
module Poepod
|
6
|
+
# Processes files for concatenation, handling binary and dot files
|
12
7
|
class FileProcessor < Processor
|
13
8
|
EXCLUDE_DEFAULT = [
|
14
|
-
%r{node_modules/}, %r{.git/}, /.gitignore$/, /.DS_Store
|
9
|
+
%r{node_modules/}, %r{.git/}, /.gitignore$/, /.DS_Store$/, /^\..+/
|
15
10
|
].freeze
|
16
11
|
|
17
|
-
def initialize(
|
18
|
-
|
12
|
+
def initialize(
|
13
|
+
files,
|
14
|
+
output_file,
|
15
|
+
config_file: nil,
|
16
|
+
include_binary: false,
|
17
|
+
include_dot_files: false,
|
18
|
+
exclude: [],
|
19
|
+
base_dir: nil
|
20
|
+
)
|
21
|
+
super(
|
22
|
+
config_file,
|
23
|
+
include_binary: include_binary,
|
24
|
+
include_dot_files: include_dot_files,
|
25
|
+
exclude: exclude,
|
26
|
+
base_dir: base_dir,
|
27
|
+
)
|
19
28
|
@files = files
|
20
29
|
@output_file = output_file
|
21
|
-
@failed_files = []
|
22
|
-
@include_binary = include_binary
|
23
30
|
end
|
24
31
|
|
25
|
-
|
26
|
-
total_files = 0
|
27
|
-
copied_files = 0
|
32
|
+
private
|
28
33
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
34
|
+
def collect_files_to_process
|
35
|
+
@files.flatten.each_with_object([]) do |file, files_to_process|
|
36
|
+
Dir.glob(file, File::FNM_DOTMATCH).each do |matched_file|
|
37
|
+
next unless File.file?(matched_file)
|
38
|
+
next if should_exclude?(matched_file)
|
33
39
|
|
34
|
-
|
35
|
-
file_path, content, error = process_file(matched_file)
|
36
|
-
if content
|
37
|
-
output.puts "--- START FILE: #{file_path} ---"
|
38
|
-
output.puts content
|
39
|
-
output.puts "--- END FILE: #{file_path} ---"
|
40
|
-
copied_files += 1
|
41
|
-
elsif error
|
42
|
-
output.puts "#{file_path}\n#{error}"
|
43
|
-
end
|
44
|
-
end
|
40
|
+
files_to_process << matched_file
|
45
41
|
end
|
46
42
|
end
|
47
|
-
|
48
|
-
[total_files, copied_files]
|
49
|
-
end
|
50
|
-
|
51
|
-
private
|
52
|
-
|
53
|
-
def process_file(file_path)
|
54
|
-
if text_file?(file_path)
|
55
|
-
content = File.read(file_path, encoding: "utf-8")
|
56
|
-
[file_path, content, nil]
|
57
|
-
elsif @include_binary
|
58
|
-
content = encode_binary_file(file_path)
|
59
|
-
[file_path, content, nil]
|
60
|
-
else
|
61
|
-
[file_path, nil, "Skipped binary file"]
|
62
|
-
end
|
63
|
-
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
64
|
-
@failed_files << file_path
|
65
|
-
[file_path, nil, "Failed to decode the file, as it is not saved with UTF-8 encoding."]
|
66
|
-
end
|
67
|
-
|
68
|
-
def text_file?(file_path)
|
69
|
-
stdout, status = Open3.capture2("file", "-b", "--mime-type", file_path)
|
70
|
-
status.success? && stdout.strip.start_with?("text/")
|
71
|
-
end
|
72
|
-
|
73
|
-
def encode_binary_file(file_path)
|
74
|
-
mime_type = MIME::Types.type_for(file_path).first.content_type
|
75
|
-
encoded_content = Base64.strict_encode64(File.binread(file_path))
|
76
|
-
"Content-Type: #{mime_type}\nContent-Transfer-Encoding: base64\n\n#{encoded_content}"
|
77
43
|
end
|
78
44
|
end
|
79
45
|
end
|
data/lib/poepod/gem_processor.rb
CHANGED
@@ -6,61 +6,76 @@ require "rubygems/specification"
|
|
6
6
|
require "git"
|
7
7
|
|
8
8
|
module Poepod
|
9
|
+
# Processes gem files for wrapping, handling unstaged files
|
9
10
|
class GemProcessor < Processor
|
10
|
-
def initialize(
|
11
|
-
|
11
|
+
def initialize(
|
12
|
+
gemspec_path,
|
13
|
+
include_unstaged: false,
|
14
|
+
exclude: [],
|
15
|
+
include_binary: false,
|
16
|
+
include_dot_files: false,
|
17
|
+
base_dir: nil,
|
18
|
+
config_file: nil
|
19
|
+
)
|
20
|
+
super(
|
21
|
+
config_file,
|
22
|
+
include_binary: include_binary,
|
23
|
+
include_dot_files: include_dot_files,
|
24
|
+
exclude: exclude,
|
25
|
+
base_dir: base_dir || File.dirname(gemspec_path),
|
26
|
+
)
|
12
27
|
@gemspec_path = gemspec_path
|
13
28
|
@include_unstaged = include_unstaged
|
14
29
|
end
|
15
30
|
|
16
31
|
def process
|
17
|
-
unless File.exist?(@gemspec_path)
|
18
|
-
return [false, "Error: The specified gemspec file '#{@gemspec_path}' does not exist."]
|
19
|
-
end
|
32
|
+
return error_no_gemspec unless File.exist?(@gemspec_path)
|
20
33
|
|
21
|
-
|
22
|
-
|
23
|
-
rescue StandardError => e
|
24
|
-
return [false, "Error loading gemspec: #{e.message}"]
|
25
|
-
end
|
34
|
+
spec = load_gemspec
|
35
|
+
return spec unless spec.is_a?(Gem::Specification)
|
26
36
|
|
27
37
|
gem_name = spec.name
|
28
|
-
output_file = "#{gem_name}_wrapped.txt"
|
38
|
+
@output_file = "#{gem_name}_wrapped.txt"
|
29
39
|
unstaged_files = check_unstaged_files
|
30
40
|
|
31
|
-
|
32
|
-
file.puts "# Wrapped Gem: #{gem_name}"
|
33
|
-
file.puts "## Gemspec: #{File.basename(@gemspec_path)}"
|
41
|
+
super()
|
34
42
|
|
35
|
-
|
36
|
-
|
37
|
-
file.puts unstaged_files.join("\n")
|
38
|
-
file.puts "\nThese files are not included in the wrap unless --include-unstaged option is used."
|
39
|
-
end
|
43
|
+
[true, @output_file, unstaged_files]
|
44
|
+
end
|
40
45
|
|
41
|
-
|
46
|
+
private
|
42
47
|
|
43
|
-
|
44
|
-
|
48
|
+
def collect_files_to_process
|
49
|
+
spec = load_gemspec
|
50
|
+
files_to_include = (spec.files +
|
51
|
+
spec.test_files +
|
52
|
+
find_readme_files).uniq
|
45
53
|
|
46
|
-
|
47
|
-
full_path = File.join(File.dirname(@gemspec_path), relative_path)
|
48
|
-
next unless File.file?(full_path)
|
54
|
+
files_to_include += check_unstaged_files if @include_unstaged
|
49
55
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
56
|
+
files_to_include.sort.uniq.reject do |relative_path|
|
57
|
+
should_exclude?(File.join(@base_dir, relative_path))
|
58
|
+
end.map do |relative_path|
|
59
|
+
File.join(@base_dir, relative_path)
|
54
60
|
end
|
61
|
+
end
|
55
62
|
|
56
|
-
|
63
|
+
def error_no_gemspec
|
64
|
+
[false, "Error: The specified gemspec file '#{@gemspec_path}' does not exist."]
|
57
65
|
end
|
58
66
|
|
59
|
-
|
67
|
+
def load_gemspec
|
68
|
+
Gem::Specification.load(@gemspec_path)
|
69
|
+
rescue StandardError => e
|
70
|
+
[false, "Error loading gemspec: #{e.message}"]
|
71
|
+
end
|
60
72
|
|
61
73
|
def find_readme_files
|
62
|
-
Dir.glob(File.join(File.dirname(@gemspec_path), "README*"))
|
63
|
-
|
74
|
+
Dir.glob(File.join(File.dirname(@gemspec_path), "README*")).map do |path|
|
75
|
+
Pathname.new(path).relative_path_from(
|
76
|
+
Pathname.new(File.dirname(@gemspec_path))
|
77
|
+
).to_s
|
78
|
+
end
|
64
79
|
end
|
65
80
|
|
66
81
|
def check_unstaged_files
|
data/lib/poepod/processor.rb
CHANGED
@@ -1,22 +1,123 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require "yaml"
|
4
|
+
require "base64"
|
5
|
+
require "marcel"
|
6
|
+
require "stringio"
|
7
|
+
|
4
8
|
module Poepod
|
9
|
+
# Base processor class
|
5
10
|
class Processor
|
6
|
-
def initialize(
|
11
|
+
def initialize(
|
12
|
+
config_file = nil,
|
13
|
+
include_binary: false,
|
14
|
+
include_dot_files: false,
|
15
|
+
exclude: [],
|
16
|
+
base_dir: nil
|
17
|
+
)
|
7
18
|
@config = load_config(config_file)
|
19
|
+
@include_binary = include_binary
|
20
|
+
@include_dot_files = include_dot_files
|
21
|
+
@exclude = exclude || []
|
22
|
+
@base_dir = base_dir
|
23
|
+
@failed_files = []
|
24
|
+
end
|
25
|
+
|
26
|
+
def process
|
27
|
+
files_to_process = collect_files_to_process
|
28
|
+
total_files, copied_files = process_files(files_to_process)
|
29
|
+
[total_files, copied_files]
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def collect_files_to_process
|
35
|
+
raise NotImplementedError, "Subclasses must implement collect_files_to_process"
|
8
36
|
end
|
9
37
|
|
10
38
|
def load_config(config_file)
|
11
|
-
|
12
|
-
|
39
|
+
return {} unless config_file && File.exist?(config_file)
|
40
|
+
|
41
|
+
YAML.load_file(config_file)
|
42
|
+
end
|
43
|
+
|
44
|
+
def binary_file?(file_path)
|
45
|
+
return false unless File.exist?(file_path) && File.file?(file_path)
|
46
|
+
|
47
|
+
File.open(file_path, "rb") do |file|
|
48
|
+
content = file.read(8192) # Read first 8KB for magic byte detection
|
49
|
+
mime_type = Marcel::MimeType.for(content, name: File.basename(file_path), declared_type: "text/plain")
|
50
|
+
!mime_type.start_with?("text/") && mime_type != "application/json"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def process_files(files)
|
55
|
+
total_files = files.size
|
56
|
+
copied_files = 0
|
57
|
+
|
58
|
+
File.open(@output_file, "w", encoding: "utf-8") do |output|
|
59
|
+
files.sort.each do |file_path|
|
60
|
+
process_file(output, file_path)
|
61
|
+
copied_files += 1
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
[total_files, copied_files]
|
66
|
+
end
|
67
|
+
|
68
|
+
def process_file(output = nil, file_path)
|
69
|
+
output ||= StringIO.new
|
70
|
+
|
71
|
+
relative_path = if @base_dir
|
72
|
+
Pathname.new(file_path).relative_path_from(Pathname.new(@base_dir)).to_s
|
73
|
+
else
|
74
|
+
file_path
|
75
|
+
end
|
76
|
+
|
77
|
+
output.puts "--- START FILE: #{relative_path} ---"
|
78
|
+
|
79
|
+
if binary_file?(file_path) && @include_binary
|
80
|
+
output.puts encode_binary_file(file_path)
|
13
81
|
else
|
14
|
-
|
82
|
+
output.puts File.read(file_path)
|
15
83
|
end
|
84
|
+
|
85
|
+
output.puts "--- END FILE: #{relative_path} ---\n"
|
86
|
+
|
87
|
+
output.string if output.is_a?(StringIO) # Return the string if using StringIO
|
16
88
|
end
|
17
89
|
|
18
|
-
def
|
19
|
-
|
90
|
+
def encode_binary_file(file_path)
|
91
|
+
content = File.binread(file_path)
|
92
|
+
mime_type = Marcel::MimeType.for(content, name: File.basename(file_path))
|
93
|
+
encoded_content = Base64.strict_encode64(content)
|
94
|
+
<<~HERE
|
95
|
+
Content-Type: #{mime_type}
|
96
|
+
Content-Transfer-Encoding: base64
|
97
|
+
|
98
|
+
#{encoded_content}
|
99
|
+
HERE
|
100
|
+
end
|
101
|
+
|
102
|
+
def dot_file?(file_path)
|
103
|
+
File.basename(file_path).start_with?(".")
|
104
|
+
end
|
105
|
+
|
106
|
+
def should_exclude?(file_path)
|
107
|
+
return true if !@include_dot_files && dot_file?(file_path)
|
108
|
+
return true if !@include_binary && binary_file?(file_path)
|
109
|
+
|
110
|
+
exclude_file?(file_path)
|
111
|
+
end
|
112
|
+
|
113
|
+
def exclude_file?(file_path)
|
114
|
+
@exclude.any? do |pattern|
|
115
|
+
if pattern.is_a?(Regexp)
|
116
|
+
file_path.match?(pattern)
|
117
|
+
else
|
118
|
+
File.fnmatch?(pattern, file_path)
|
119
|
+
end
|
120
|
+
end
|
20
121
|
end
|
21
122
|
end
|
22
123
|
end
|
data/lib/poepod/version.rb
CHANGED
data/poepod.gemspec
CHANGED
@@ -31,7 +31,7 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.test_files = `git ls-files -- spec/*`.split("\n")
|
32
32
|
|
33
33
|
spec.add_runtime_dependency "git", "~> 1.11"
|
34
|
-
spec.add_runtime_dependency "
|
34
|
+
spec.add_runtime_dependency "marcel", "~> 1.0"
|
35
35
|
spec.add_runtime_dependency "parallel", "~> 1.20"
|
36
36
|
spec.add_runtime_dependency "thor", "~> 1.0"
|
37
37
|
spec.add_runtime_dependency "tqdm"
|
data/spec/poepod/cli_spec.rb
CHANGED
@@ -10,37 +10,62 @@ RSpec.describe Poepod::Cli do
|
|
10
10
|
let(:temp_dir) { Dir.mktmpdir }
|
11
11
|
let(:text_file) { File.join(temp_dir, "text_file.txt") }
|
12
12
|
let(:binary_file) { File.join(temp_dir, "binary_file.bin") }
|
13
|
+
let(:dot_file) { File.join(temp_dir, ".hidden_file") }
|
13
14
|
|
14
15
|
before do
|
15
16
|
File.write(text_file, "Hello, World!")
|
16
17
|
File.write(binary_file, [0xFF, 0xD8, 0xFF, 0xE0].pack("C*"))
|
18
|
+
File.write(dot_file, "Hidden content")
|
17
19
|
end
|
18
20
|
|
19
21
|
after do
|
20
22
|
FileUtils.remove_entry(temp_dir)
|
21
23
|
end
|
22
24
|
|
23
|
-
it "concatenates text files" do
|
25
|
+
it "concatenates text files and excludes binary and dot files by default" do
|
24
26
|
output_file = File.join(temp_dir, "output.txt")
|
25
|
-
expect
|
27
|
+
expect do
|
28
|
+
cli.invoke(:concat, [File.join(temp_dir, "*")], { output_file: output_file })
|
29
|
+
end.to output(/1 files detected\.\n.*1 files have been concatenated/).to_stdout
|
26
30
|
expect(File.exist?(output_file)).to be true
|
27
|
-
|
31
|
+
content = File.read(output_file)
|
32
|
+
expect(content).to include("Hello, World!")
|
33
|
+
expect(content).not_to include("Hidden content")
|
28
34
|
end
|
29
35
|
|
30
|
-
it "
|
36
|
+
it "includes binary files when specified" do
|
31
37
|
output_file = File.join(temp_dir, "output.txt")
|
32
38
|
expect do
|
33
|
-
cli.concat(
|
34
|
-
|
35
|
-
|
39
|
+
cli.invoke(:concat, [File.join(temp_dir, "*")], { output_file: output_file, include_binary: true })
|
40
|
+
end.to output(/2 files detected\.\n.*2 files have been concatenated/).to_stdout
|
41
|
+
expect(File.exist?(output_file)).to be true
|
42
|
+
content = File.read(output_file)
|
43
|
+
expect(content).to include("Hello, World!")
|
44
|
+
expect(content).to include("Content-Type: image/jpeg")
|
36
45
|
end
|
37
46
|
|
38
|
-
it "includes
|
47
|
+
it "includes dot files when specified" do
|
39
48
|
output_file = File.join(temp_dir, "output.txt")
|
40
49
|
expect do
|
41
|
-
cli.invoke(:concat, [
|
42
|
-
|
43
|
-
|
50
|
+
cli.invoke(:concat, [File.join(temp_dir, "*")], { output_file: output_file, include_dot_files: true })
|
51
|
+
end.to output(/2 files detected\.\n.*2 files have been concatenated/).to_stdout
|
52
|
+
expect(File.exist?(output_file)).to be true
|
53
|
+
content = File.read(output_file)
|
54
|
+
expect(content).to include("Hello, World!")
|
55
|
+
expect(content).to include("Hidden content")
|
56
|
+
end
|
57
|
+
|
58
|
+
it "uses the specified base directory for relative paths" do
|
59
|
+
output_file = File.join(temp_dir, "output.txt")
|
60
|
+
base_dir = File.dirname(text_file)
|
61
|
+
expect do
|
62
|
+
cli.invoke(:concat, [File.join(temp_dir, "*")], { output_file: output_file, base_dir: base_dir })
|
63
|
+
end.to output(/1 files detected\.\n.*1 files have been concatenated/).to_stdout
|
64
|
+
expect(File.exist?(output_file)).to be true
|
65
|
+
content = File.read(output_file)
|
66
|
+
expect(content).to include("--- START FILE: text_file.txt ---")
|
67
|
+
expect(content).to include("Hello, World!")
|
68
|
+
expect(content).to include("--- END FILE: text_file.txt ---")
|
44
69
|
end
|
45
70
|
end
|
46
71
|
|
@@ -74,8 +99,6 @@ RSpec.describe Poepod::Cli do
|
|
74
99
|
output_file = File.join(Dir.pwd, "test_gem_wrapped.txt")
|
75
100
|
expect(File.exist?(output_file)).to be true
|
76
101
|
content = File.read(output_file)
|
77
|
-
expect(content).to include("# Wrapped Gem: test_gem")
|
78
|
-
expect(content).to include("## Gemspec: test_gem.gemspec")
|
79
102
|
expect(content).to include("--- START FILE: lib/test_gem.rb ---")
|
80
103
|
expect(content).to include("puts 'Hello from test_gem'")
|
81
104
|
expect(content).to include("--- END FILE: lib/test_gem.rb ---")
|
@@ -86,5 +109,18 @@ RSpec.describe Poepod::Cli do
|
|
86
109
|
cli.wrap("non_existent.gemspec")
|
87
110
|
end.to output(/Error: The specified gemspec file/).to_stdout.and raise_error(SystemExit)
|
88
111
|
end
|
112
|
+
|
113
|
+
it "uses the specified base directory for relative paths" do
|
114
|
+
base_dir = File.dirname(gemspec_file)
|
115
|
+
expect do
|
116
|
+
cli.invoke(:wrap, [gemspec_file], { base_dir: base_dir })
|
117
|
+
end.to output(/The gem has been wrapped into/).to_stdout
|
118
|
+
output_file = File.join(Dir.pwd, "test_gem_wrapped.txt")
|
119
|
+
expect(File.exist?(output_file)).to be true
|
120
|
+
content = File.read(output_file)
|
121
|
+
expect(content).to include("--- START FILE: lib/test_gem.rb ---")
|
122
|
+
expect(content).to include("puts 'Hello from test_gem'")
|
123
|
+
expect(content).to include("--- END FILE: lib/test_gem.rb ---")
|
124
|
+
end
|
89
125
|
end
|
90
126
|
end
|
@@ -11,11 +11,13 @@ RSpec.describe Poepod::FileProcessor do
|
|
11
11
|
let(:text_file1) { File.join(temp_dir, "file1.txt") }
|
12
12
|
let(:text_file2) { File.join(temp_dir, "file2.txt") }
|
13
13
|
let(:binary_file) { File.join(temp_dir, "binary_file.bin") }
|
14
|
+
let(:dot_file) { File.join(temp_dir, ".hidden_file") }
|
14
15
|
|
15
16
|
before do
|
16
17
|
File.write(text_file1, "Content of file1.\n")
|
17
18
|
File.write(text_file2, "Content of file2.\n")
|
18
19
|
File.write(binary_file, [0xFF, 0xD8, 0xFF, 0xE0].pack("C*"))
|
20
|
+
File.write(dot_file, "Content of hidden file.\n")
|
19
21
|
end
|
20
22
|
|
21
23
|
after do
|
@@ -23,41 +25,126 @@ RSpec.describe Poepod::FileProcessor do
|
|
23
25
|
output_file.unlink
|
24
26
|
end
|
25
27
|
|
26
|
-
let(:processor) { described_class.new([text_file1, text_file2], output_file.path) }
|
27
|
-
|
28
28
|
describe "#process" do
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
29
|
+
context "with default options" do
|
30
|
+
let(:processor) { described_class.new([File.join(temp_dir, "*")], output_file.path) }
|
31
|
+
|
32
|
+
it "processes text files and excludes binary and dot files" do
|
33
|
+
total_files, copied_files = processor.process
|
34
|
+
expect(total_files).to eq(2)
|
35
|
+
expect(copied_files).to eq(2)
|
36
|
+
|
37
|
+
output_content = File.read(output_file.path, encoding: "utf-8")
|
38
|
+
expected_content = <<~TEXT
|
39
|
+
--- START FILE: #{text_file1} ---
|
40
|
+
Content of file1.
|
41
|
+
--- END FILE: #{text_file1} ---
|
42
|
+
--- START FILE: #{text_file2} ---
|
43
|
+
Content of file2.
|
44
|
+
--- END FILE: #{text_file2} ---
|
45
|
+
TEXT
|
46
|
+
expect(output_content).to eq(expected_content)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "with include_binary option" do
|
51
|
+
let(:processor) { described_class.new([File.join(temp_dir, "*")], output_file.path, include_binary: true) }
|
52
|
+
|
53
|
+
it "includes binary files" do
|
54
|
+
total_files, copied_files = processor.process
|
55
|
+
expect(total_files).to eq(3)
|
56
|
+
expect(copied_files).to eq(3)
|
57
|
+
|
58
|
+
output_content = File.read(output_file.path, encoding: "utf-8")
|
59
|
+
expected_content = <<~TEXT
|
60
|
+
--- START FILE: #{binary_file} ---
|
61
|
+
Content-Type: image/jpeg
|
62
|
+
Content-Transfer-Encoding: base64
|
63
|
+
|
64
|
+
/9j/4A==
|
65
|
+
--- END FILE: #{binary_file} ---
|
66
|
+
--- START FILE: #{text_file1} ---
|
67
|
+
Content of file1.
|
68
|
+
--- END FILE: #{text_file1} ---
|
69
|
+
--- START FILE: #{text_file2} ---
|
70
|
+
Content of file2.
|
71
|
+
--- END FILE: #{text_file2} ---
|
72
|
+
TEXT
|
73
|
+
expect(output_content).to eq(expected_content)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context "with include_dot_files option" do
|
78
|
+
let(:processor) { described_class.new([File.join(temp_dir, "*")], output_file.path, include_dot_files: true) }
|
79
|
+
|
80
|
+
it "includes dot files" do
|
81
|
+
total_files, copied_files = processor.process
|
82
|
+
expect(total_files).to eq(3)
|
83
|
+
expect(copied_files).to eq(3)
|
84
|
+
|
85
|
+
output_content = File.read(output_file.path, encoding: "utf-8")
|
86
|
+
expected_content = <<~TEXT
|
87
|
+
--- START FILE: #{dot_file} ---
|
88
|
+
Content of hidden file.
|
89
|
+
--- END FILE: #{dot_file} ---
|
90
|
+
--- START FILE: #{text_file1} ---
|
91
|
+
Content of file1.
|
92
|
+
--- END FILE: #{text_file1} ---
|
93
|
+
--- START FILE: #{text_file2} ---
|
94
|
+
Content of file2.
|
95
|
+
--- END FILE: #{text_file2} ---
|
96
|
+
TEXT
|
97
|
+
expect(output_content).to eq(expected_content)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context "with both include_binary and include_dot_files options" do
|
102
|
+
let(:processor) do
|
103
|
+
described_class.new([File.join(temp_dir, "*")], output_file.path, include_binary: true, include_dot_files: true)
|
104
|
+
end
|
105
|
+
|
106
|
+
it "includes all files in sorted order" do
|
107
|
+
total_files, copied_files = processor.process
|
108
|
+
expect(total_files).to eq(4)
|
109
|
+
expect(copied_files).to eq(4)
|
110
|
+
|
111
|
+
output_content = File.read(output_file.path, encoding: "utf-8")
|
112
|
+
expected_content = <<~HERE
|
113
|
+
--- START FILE: #{dot_file} ---
|
114
|
+
Content of hidden file.
|
115
|
+
--- END FILE: #{dot_file} ---
|
116
|
+
--- START FILE: #{binary_file} ---
|
117
|
+
Content-Type: image/jpeg
|
118
|
+
Content-Transfer-Encoding: base64
|
119
|
+
|
120
|
+
/9j/4A==
|
121
|
+
--- END FILE: #{binary_file} ---
|
122
|
+
--- START FILE: #{text_file1} ---
|
123
|
+
Content of file1.
|
124
|
+
--- END FILE: #{text_file1} ---
|
125
|
+
--- START FILE: #{text_file2} ---
|
126
|
+
Content of file2.
|
127
|
+
--- END FILE: #{text_file2} ---
|
128
|
+
HERE
|
129
|
+
|
130
|
+
expect(output_content).to eq(expected_content)
|
131
|
+
end
|
44
132
|
end
|
45
133
|
end
|
46
134
|
|
47
135
|
describe "#process_file" do
|
136
|
+
let(:processor) { described_class.new([text_file1], output_file.path) }
|
137
|
+
|
48
138
|
it "reads the content of a file" do
|
49
|
-
|
50
|
-
expect(
|
51
|
-
expect(content).to eq("Content of file1.\n")
|
52
|
-
expect(error).to be_nil
|
139
|
+
content = processor.send(:process_file, nil, text_file1)
|
140
|
+
expect(content).to include("Content of file1.\n")
|
53
141
|
end
|
54
142
|
|
55
143
|
it "handles encoding errors gracefully" do
|
56
144
|
allow(File).to receive(:read).and_raise(Encoding::InvalidByteSequenceError)
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
expect(error).to eq("Failed to decode the file, as it is not saved with UTF-8 encoding.")
|
145
|
+
expect do
|
146
|
+
processor.send(:process_file, nil, text_file1)
|
147
|
+
end.to raise_error(Encoding::InvalidByteSequenceError)
|
61
148
|
end
|
62
149
|
end
|
63
150
|
end
|
@@ -40,26 +40,42 @@ RSpec.describe Poepod::GemProcessor do
|
|
40
40
|
allow(Git).to receive(:open).and_return(double(status: double(untracked: {}, changed: {})))
|
41
41
|
end
|
42
42
|
|
43
|
-
it "processes the gem files, includes README files, and spec files" do
|
43
|
+
it "processes the gem files, includes README files, and spec files in sorted order" do
|
44
44
|
success, output_file = processor.process
|
45
45
|
expect(success).to be true
|
46
46
|
expect(File.exist?(output_file)).to be true
|
47
47
|
|
48
48
|
content = File.read(output_file)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
expect(
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
49
|
+
|
50
|
+
file_order = content.scan(/--- START FILE: (.+) ---/).flatten
|
51
|
+
expected_order = [
|
52
|
+
"README.md",
|
53
|
+
"README.txt",
|
54
|
+
"lib/test_gem.rb",
|
55
|
+
"spec/test_gem_spec.rb"
|
56
|
+
]
|
57
|
+
expect(file_order).to eq(expected_order)
|
58
|
+
|
59
|
+
expected = <<~HERE
|
60
|
+
--- START FILE: README.md ---
|
61
|
+
# Test Gem
|
62
|
+
|
63
|
+
This is a test gem.
|
64
|
+
--- END FILE: README.md ---
|
65
|
+
--- START FILE: README.txt ---
|
66
|
+
Test Gem
|
67
|
+
|
68
|
+
This is a test gem in plain text.
|
69
|
+
--- END FILE: README.txt ---
|
70
|
+
--- START FILE: lib/test_gem.rb ---
|
71
|
+
puts 'Hello from test_gem'
|
72
|
+
--- END FILE: lib/test_gem.rb ---
|
73
|
+
--- START FILE: spec/test_gem_spec.rb ---
|
74
|
+
RSpec.describe TestGem do
|
75
|
+
end
|
76
|
+
--- END FILE: spec/test_gem_spec.rb ---
|
77
|
+
HERE
|
78
|
+
expect(content).to eq(expected)
|
63
79
|
end
|
64
80
|
|
65
81
|
context "with non-existent gemspec" do
|
@@ -73,28 +89,21 @@ RSpec.describe Poepod::GemProcessor do
|
|
73
89
|
end
|
74
90
|
|
75
91
|
context "with unstaged files" do
|
92
|
+
let(:processor) { described_class.new(gemspec_file, include_unstaged: false) }
|
76
93
|
let(:mock_git) { instance_double(Git::Base) }
|
77
94
|
let(:mock_status) { instance_double(Git::Status) }
|
78
95
|
|
79
96
|
before do
|
80
97
|
allow(Git).to receive(:open).and_return(mock_git)
|
81
98
|
allow(mock_git).to receive(:status).and_return(mock_status)
|
82
|
-
allow(mock_status).to receive(:untracked).and_return(
|
99
|
+
allow(mock_status).to receive(:untracked).and_return(
|
100
|
+
{ "lib/unstaged_file.rb" => "??" }
|
101
|
+
)
|
83
102
|
allow(mock_status).to receive(:changed).and_return({})
|
84
103
|
end
|
85
104
|
|
86
|
-
it "warns about unstaged files" do
|
87
|
-
success, output_file, unstaged_files = processor.process
|
88
|
-
expect(success).to be true
|
89
|
-
expect(unstaged_files).to eq(["lib/unstaged_file.rb"])
|
90
|
-
|
91
|
-
content = File.read(output_file)
|
92
|
-
expect(content).to include("## Warning: Unstaged Files")
|
93
|
-
expect(content).to include("lib/unstaged_file.rb")
|
94
|
-
end
|
95
|
-
|
96
105
|
context "with include_unstaged option" do
|
97
|
-
let(:processor) { described_class.new(gemspec_file,
|
106
|
+
let(:processor) { described_class.new(gemspec_file, include_unstaged: true) }
|
98
107
|
|
99
108
|
it "includes unstaged files" do
|
100
109
|
allow(File).to receive(:file?).and_return(true)
|
@@ -115,17 +124,13 @@ RSpec.describe Poepod::GemProcessor do
|
|
115
124
|
file_contents[file_name]
|
116
125
|
elsif path.end_with?("_wrapped.txt")
|
117
126
|
# This is the output file, so we'll construct its content here
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
wrapped_content += "#{content}\n"
|
126
|
-
wrapped_content += "--- END FILE: #{file} ---\n\n"
|
127
|
-
end
|
128
|
-
wrapped_content
|
127
|
+
file_contents.map do |file, content|
|
128
|
+
<<~HERE
|
129
|
+
--- START FILE: #{file} ---
|
130
|
+
#{content}
|
131
|
+
--- END FILE: #{file} ---
|
132
|
+
HERE
|
133
|
+
end.join("")
|
129
134
|
else
|
130
135
|
"Default content for #{path}"
|
131
136
|
end
|
@@ -136,21 +141,29 @@ RSpec.describe Poepod::GemProcessor do
|
|
136
141
|
expect(unstaged_files).to eq(["lib/unstaged_file.rb"])
|
137
142
|
|
138
143
|
content = File.read(output_file)
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
144
|
+
expected = <<~HERE
|
145
|
+
--- START FILE: lib/test_gem.rb ---
|
146
|
+
puts 'Hello from test_gem'
|
147
|
+
--- END FILE: lib/test_gem.rb ---
|
148
|
+
--- START FILE: spec/test_gem_spec.rb ---
|
149
|
+
RSpec.describe TestGem do
|
150
|
+
end
|
151
|
+
--- END FILE: spec/test_gem_spec.rb ---
|
152
|
+
--- START FILE: README.md ---
|
153
|
+
# Test Gem
|
154
|
+
|
155
|
+
This is a test gem.
|
156
|
+
--- END FILE: README.md ---
|
157
|
+
--- START FILE: README.txt ---
|
158
|
+
Test Gem
|
159
|
+
|
160
|
+
This is a test gem in plain text.
|
161
|
+
--- END FILE: README.txt ---
|
162
|
+
--- START FILE: lib/unstaged_file.rb ---
|
163
|
+
Unstaged content
|
164
|
+
--- END FILE: lib/unstaged_file.rb ---
|
165
|
+
HERE
|
166
|
+
expect(content).to eq(expected)
|
154
167
|
end
|
155
168
|
end
|
156
169
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: poepod
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose Inc.
|
@@ -25,19 +25,19 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.11'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: marcel
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '1.0'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '1.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: parallel
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|