poepod 0.1.5 → 0.1.6

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 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