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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: db687d7a65f167645bedbabce5082c8d254a70f891b493a7f900a4666b07dfc3
4
- data.tar.gz: 159ed36950788d819b870a2f8d5e8d1e7c26103cdadc11702073bca959885711
3
+ metadata.gz: b205fe50c5830d75893fbfa378b81984bf8a2b73fa8d104509fbbf698e0001fc
4
+ data.tar.gz: d4e87cef72c92cdcfaadd6bcf5a452bc1a05b13c4927cce10b6c81250ff99c9c
5
5
  SHA512:
6
- metadata.gz: 53e110b148f0e78e7f9c257f18619f4c5718d01d390c8a604fd9a928349a25ca8840a26ac6130dc793f7687783732d360100c96195b88f83a2af20b58bed7723
7
- data.tar.gz: 007e29ab64c3c3984aaef34a700fe3384c97d3f784f93cda466d8335252b979829b0373d7802737990bf8e65c618fa2e00fd46970a2345e57c35a24a33ddc05d
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. This is particularly useful when you want to review or analyze code from
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 `output.txt`.
80
+ This will concatenate all non-binary, non-dot files from the specified path into
81
+ `output.txt`.
58
82
 
59
- ==== Excluding patterns
83
+ ==== Including dot files
60
84
 
61
- You can exclude certain patterns using the `--exclude` option:
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 --exclude node_modules .git build test
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. However, you can include binary files (encoded in MIME format) using the
75
- `--include-binary` option:
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
- desc "concat FILES [OUTPUT_FILE]", "Concatenate specified files into one text file"
11
- option :exclude, type: :array, default: Poepod::FileProcessor::EXCLUDE_DEFAULT, desc: "List of patterns to exclude"
12
- option :config, type: :string, desc: "Path to configuration file"
13
- option :include_binary, type: :boolean, default: false, desc: "Include binary files (encoded in MIME format)"
14
-
15
- def concat(*files, output_file: nil)
16
- if files.empty?
17
- puts "Error: No files specified."
18
- exit(1)
19
- end
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
- processor = Poepod::FileProcessor.new(files, output_path, options[:config], options[:include_binary])
25
- total_files, copied_files = processor.process
22
+ desc "concat FILES [OUTPUT_FILE]", "Concatenate specified files into one text file"
23
+ shared_options
26
24
 
27
- puts "-> #{total_files} files detected."
28
- puts "=> #{copied_files} files have been concatenated into #{output_path.relative_path_from(Dir.pwd)}."
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
- processor = Poepod::GemProcessor.new(gemspec_path, nil, options[:include_unstaged])
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
- puts "=> The gem has been wrapped into '#{result}'."
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, ".*")}_concat.txt"
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(files, output_file, config_file = nil, include_binary = false)
18
- super(config_file)
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
- def process
26
- total_files = 0
27
- copied_files = 0
32
+ private
28
33
 
29
- File.open(@output_file, "w", encoding: "utf-8") do |output|
30
- @files.each do |file|
31
- Dir.glob(file).each do |matched_file|
32
- next unless File.file?(matched_file)
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
- total_files += 1
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
@@ -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(gemspec_path, config_file = nil, include_unstaged = false)
11
- super(config_file)
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
- begin
22
- spec = Gem::Specification.load(@gemspec_path)
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
- File.open(output_file, "w") do |file|
32
- file.puts "# Wrapped Gem: #{gem_name}"
33
- file.puts "## Gemspec: #{File.basename(@gemspec_path)}"
41
+ super()
34
42
 
35
- if unstaged_files.any?
36
- file.puts "\n## Warning: Unstaged Files"
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
- file.puts "\n## Files:\n"
46
+ private
42
47
 
43
- files_to_include = (spec.files + spec.test_files + find_readme_files).uniq
44
- files_to_include += unstaged_files if @include_unstaged
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
- files_to_include.uniq.each do |relative_path|
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
- file.puts "--- START FILE: #{relative_path} ---"
51
- file.puts File.read(full_path)
52
- file.puts "--- END FILE: #{relative_path} ---\n\n"
53
- end
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
- [true, output_file, unstaged_files]
63
+ def error_no_gemspec
64
+ [false, "Error: The specified gemspec file '#{@gemspec_path}' does not exist."]
57
65
  end
58
66
 
59
- private
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
- .map { |path| Pathname.new(path).relative_path_from(Pathname.new(File.dirname(@gemspec_path))).to_s }
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
@@ -1,22 +1,123 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/poepod/processor.rb
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(config_file = nil)
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
- if config_file && File.exist?(config_file)
12
- YAML.load_file(config_file)
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 process
19
- raise NotImplementedError, "Subclasses must implement the 'process' method"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Poepod
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.6"
5
5
  end
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 "mime-types", "~> 3.3"
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"
@@ -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 { cli.concat(text_file, output_file: output_file) }.to output(/1 files detected/).to_stdout
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
- expect(File.read(output_file)).to include("Hello, World!")
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 "excludes binary files by default" do
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(text_file, binary_file,
34
- output_file: output_file)
35
- end.to output(/-> 2 files detected\.\n=> 1 files have been concatenated into.*\.txt/).to_stdout
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 binary files when specified" do
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, [text_file, binary_file], output_file: output_file,
42
- include_binary: true)
43
- end.to output(/-> 2 files detected\.\n=> 2 files have been concatenated into.*\.txt/).to_stdout
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
- it "processes text files and writes them to the output file" do
30
- total_files, copied_files = processor.process
31
- expect(total_files).to eq(2)
32
- expect(copied_files).to eq(2)
33
-
34
- output_content = File.read(output_file.path, encoding: "utf-8")
35
- expected_content = <<~TEXT
36
- --- START FILE: #{text_file1} ---
37
- Content of file1.
38
- --- END FILE: #{text_file1} ---
39
- --- START FILE: #{text_file2} ---
40
- Content of file2.
41
- --- END FILE: #{text_file2} ---
42
- TEXT
43
- expect(output_content).to eq(expected_content)
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
- file_path, content, error = processor.send(:process_file, text_file1)
50
- expect(file_path).to eq(text_file1)
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
- file_path, content, error = processor.send(:process_file, text_file1)
58
- expect(file_path).to eq(text_file1)
59
- expect(content).to be_nil
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
- expect(content).to include("# Wrapped Gem: test_gem")
50
- expect(content).to include("## Gemspec: test_gem.gemspec")
51
- expect(content).to include("--- START FILE: lib/test_gem.rb ---")
52
- expect(content).to include("puts 'Hello from test_gem'")
53
- expect(content).to include("--- END FILE: lib/test_gem.rb ---")
54
- expect(content).to include("--- START FILE: spec/test_gem_spec.rb ---")
55
- expect(content).to include("RSpec.describe TestGem do")
56
- expect(content).to include("--- END FILE: spec/test_gem_spec.rb ---")
57
- expect(content).to include("--- START FILE: README.md ---")
58
- expect(content).to include("# Test Gem\n\nThis is a test gem.")
59
- expect(content).to include("--- END FILE: README.md ---")
60
- expect(content).to include("--- START FILE: README.txt ---")
61
- expect(content).to include("Test Gem\n\nThis is a test gem in plain text.")
62
- expect(content).to include("--- END FILE: README.txt ---")
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({ "lib/unstaged_file.rb" => "??" })
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, nil, true) }
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
- wrapped_content = "# Wrapped Gem: test_gem\n"
119
- wrapped_content += "## Gemspec: test_gem.gemspec\n\n"
120
- wrapped_content += "## Warning: Unstaged Files\n"
121
- wrapped_content += "lib/unstaged_file.rb\n\n"
122
- wrapped_content += "## Files:\n\n"
123
- file_contents.each do |file, content|
124
- wrapped_content += "--- START FILE: #{file} ---\n"
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
- expect(content).to include("--- START FILE: lib/test_gem.rb ---")
140
- expect(content).to include("puts 'Hello from test_gem'")
141
- expect(content).to include("--- END FILE: lib/test_gem.rb ---")
142
- expect(content).to include("--- START FILE: spec/test_gem_spec.rb ---")
143
- expect(content).to include("RSpec.describe TestGem do")
144
- expect(content).to include("--- END FILE: spec/test_gem_spec.rb ---")
145
- expect(content).to include("--- START FILE: README.md ---")
146
- expect(content).to include("# Test Gem\n\nThis is a test gem.")
147
- expect(content).to include("--- END FILE: README.md ---")
148
- expect(content).to include("--- START FILE: README.txt ---")
149
- expect(content).to include("Test Gem\n\nThis is a test gem in plain text.")
150
- expect(content).to include("--- END FILE: README.txt ---")
151
- expect(content).to include("--- START FILE: lib/unstaged_file.rb ---")
152
- expect(content).to include("Unstaged content")
153
- expect(content).to include("--- END FILE: lib/unstaged_file.rb ---")
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.5
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: mime-types
28
+ name: marcel
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '3.3'
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: '3.3'
40
+ version: '1.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: parallel
43
43
  requirement: !ruby/object:Gem::Requirement