poepod 0.1.5 → 0.1.7

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: 3bc92cfee72702377260c08e2639cf32b789e54be73d3cea7896bde4bf357332
4
+ data.tar.gz: f79c5a7847f8329c2a1c5ef1718c97b7ce18852df727a5d92c2b58244da2a7c0
5
5
  SHA512:
6
- metadata.gz: 53e110b148f0e78e7f9c257f18619f4c5718d01d390c8a604fd9a928349a25ca8840a26ac6130dc793f7687783732d360100c96195b88f83a2af20b58bed7723
7
- data.tar.gz: 007e29ab64c3c3984aaef34a700fe3384c97d3f784f93cda466d8335252b979829b0373d7802737990bf8e65c618fa2e00fd46970a2345e57c35a24a33ddc05d
6
+ metadata.gz: 92da41a07f4007489ccba2dacd7a0e0eb194042916a3172d6b62a2e246a8d80eb364816ad0a5721157e1da5367071dfa34e5ca80c3e419ee2a81ddc609f17aa0
7
+ data.tar.gz: cd07a158aab095842892fbd8dd34154811be5d64d5bed0af27d3a085974a1955217ab7a1d445975c993ee30ffb57e57b679958d870785d98767babb7e18042f9
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
@@ -1,49 +1,54 @@
1
+ # lib/poepod/cli.rb
1
2
  # frozen_string_literal: true
2
3
 
3
- # lib/poepod/cli.rb
4
4
  require "thor"
5
5
  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: nil,
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])
37
- success, result, unstaged_files = processor.process
38
-
38
+ base_dir = options[:base_dir] || File.dirname(gemspec_path)
39
+ output_file = options[:output_file] || File.join(base_dir, "#{File.basename(gemspec_path, ".*")}_wrapped.txt")
40
+ processor = Poepod::GemProcessor.new(
41
+ gemspec_path,
42
+ include_unstaged: options[:include_unstaged],
43
+ exclude: options[:exclude],
44
+ include_binary: options[:include_binary],
45
+ include_dot_files: options[:include_dot_files],
46
+ base_dir: base_dir,
47
+ config_file: options[:config],
48
+ )
49
+ success, result, unstaged_files = processor.process(output_file)
39
50
  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
51
+ handle_wrap_result(success, result, unstaged_files)
47
52
  else
48
53
  puts result
49
54
  exit(1)
@@ -56,6 +61,54 @@ module Poepod
56
61
 
57
62
  private
58
63
 
64
+ def check_files(files)
65
+ return unless files.empty?
66
+
67
+ puts "Error: No files specified."
68
+ exit(1)
69
+ end
70
+
71
+ def determine_output_file(files)
72
+ options[:output_file] || default_output_file(files.first)
73
+ end
74
+
75
+ def process_files(files, output_file, base_dir)
76
+ output_path = Pathname.new(output_file).expand_path
77
+ processor = Poepod::FileProcessor.new(
78
+ files,
79
+ output_path,
80
+ config_file: options[:config],
81
+ include_binary: options[:include_binary],
82
+ include_dot_files: options[:include_dot_files],
83
+ exclude: options[:exclude],
84
+ base_dir: base_dir,
85
+ )
86
+ total_files, copied_files = processor.process(output_path.to_s)
87
+ print_result(total_files, copied_files, output_path)
88
+ end
89
+
90
+ def print_result(total_files, copied_files, output_path)
91
+ puts "-> #{total_files} files detected."
92
+ puts "=> #{copied_files} files have been concatenated into #{output_path.relative_path_from(Dir.pwd)}."
93
+ end
94
+
95
+ def handle_wrap_result(success, result, unstaged_files)
96
+ if success
97
+ puts "=> The gem has been wrapped into '#{result}'."
98
+ print_unstaged_files_warning(unstaged_files) if unstaged_files.any?
99
+ else
100
+ puts result
101
+ exit(1)
102
+ end
103
+ end
104
+
105
+ def print_unstaged_files_warning(unstaged_files)
106
+ puts "\nWarning: The following files are not staged in git:"
107
+ puts unstaged_files
108
+ puts "\nThese files are #{options[:include_unstaged] ? "included" : "not included"} in the wrap."
109
+ puts "Use --include-unstaged option to include these files." unless options[:include_unstaged]
110
+ end
111
+
59
112
  def default_output_file(first_pattern)
60
113
  first_item = Dir.glob(first_pattern).first
61
114
  if first_item
@@ -1,79 +1,37 @@
1
+ # lib/poepod/file_processor.rb
1
2
  # frozen_string_literal: true
2
3
 
3
4
  require_relative "processor"
4
- require "yaml"
5
- require "tqdm"
6
- require "pathname"
7
- require "open3"
8
- require "base64"
9
- require "mime/types"
10
5
 
11
6
  module Poepod
7
+ # Processes files for concatenation, handling binary and dot files
12
8
  class FileProcessor < Processor
13
- EXCLUDE_DEFAULT = [
14
- %r{node_modules/}, %r{.git/}, /.gitignore$/, /.DS_Store$/
15
- ].freeze
16
-
17
- def initialize(files, output_file, config_file = nil, include_binary = false)
18
- super(config_file)
19
- @files = files
9
+ def initialize(
10
+ patterns,
11
+ output_file,
12
+ config_file: nil,
13
+ include_binary: false,
14
+ include_dot_files: false,
15
+ exclude: nil,
16
+ base_dir: nil
17
+ )
18
+ super(
19
+ config_file,
20
+ include_binary: include_binary,
21
+ include_dot_files: include_dot_files,
22
+ exclude: exclude,
23
+ base_dir: base_dir,
24
+ )
25
+ @patterns = patterns
20
26
  @output_file = output_file
21
- @failed_files = []
22
- @include_binary = include_binary
23
- end
24
-
25
- def process
26
- total_files = 0
27
- copied_files = 0
28
-
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)
33
-
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
45
- end
46
- end
47
-
48
- [total_files, copied_files]
49
27
  end
50
28
 
51
29
  private
52
30
 
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"]
31
+ def collect_files_to_process
32
+ @patterns.flatten.each_with_object([]) do |pattern, files_to_process|
33
+ files_to_process.concat(collect_files_from_pattern(pattern))
62
34
  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
35
  end
78
36
  end
79
37
  end
@@ -1,66 +1,80 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # lib/poepod/gem_processor.rb
4
3
  require_relative "processor"
5
4
  require "rubygems/specification"
6
5
  require "git"
7
6
 
8
7
  module Poepod
8
+ # Processes gem files for wrapping, handling unstaged files
9
9
  class GemProcessor < Processor
10
- def initialize(gemspec_path, config_file = nil, include_unstaged = false)
11
- super(config_file)
10
+ def initialize(
11
+ gemspec_path,
12
+ include_unstaged: false,
13
+ exclude: nil,
14
+ include_binary: false,
15
+ include_dot_files: false,
16
+ base_dir: nil,
17
+ config_file: nil
18
+ )
19
+ super(
20
+ config_file,
21
+ include_binary: include_binary,
22
+ include_dot_files: include_dot_files,
23
+ exclude: exclude,
24
+ base_dir: base_dir || File.dirname(gemspec_path),
25
+ )
12
26
  @gemspec_path = gemspec_path
13
27
  @include_unstaged = include_unstaged
14
28
  end
15
29
 
16
- def process
17
- unless File.exist?(@gemspec_path)
18
- return [false, "Error: The specified gemspec file '#{@gemspec_path}' does not exist."]
19
- end
30
+ def process(output_file)
31
+ return error_no_gemspec unless File.exist?(@gemspec_path)
20
32
 
21
- begin
22
- spec = Gem::Specification.load(@gemspec_path)
23
- rescue StandardError => e
24
- return [false, "Error loading gemspec: #{e.message}"]
25
- end
33
+ spec = load_gemspec
34
+ return spec unless spec.is_a?(Gem::Specification)
26
35
 
27
- gem_name = spec.name
28
- output_file = "#{gem_name}_wrapped.txt"
29
36
  unstaged_files = check_unstaged_files
30
37
 
31
- File.open(output_file, "w") do |file|
32
- file.puts "# Wrapped Gem: #{gem_name}"
33
- file.puts "## Gemspec: #{File.basename(@gemspec_path)}"
38
+ total_files, copied_files = super(output_file)
34
39
 
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
40
-
41
- file.puts "\n## Files:\n"
40
+ [true, output_file, unstaged_files]
41
+ end
42
42
 
43
- files_to_include = (spec.files + spec.test_files + find_readme_files).uniq
44
- files_to_include += unstaged_files if @include_unstaged
43
+ private
45
44
 
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)
45
+ def collect_files_to_process
46
+ files_to_include = find_gemspec_files
47
+ files_to_include += check_unstaged_files if @include_unstaged
49
48
 
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
49
+ files_to_include.sort.uniq.reject do |relative_path|
50
+ should_exclude?(File.join(@base_dir, relative_path))
51
+ end.map do |relative_path|
52
+ File.join(@base_dir, relative_path)
54
53
  end
54
+ end
55
55
 
56
- [true, output_file, unstaged_files]
56
+ def find_gemspec_files
57
+ spec = load_gemspec
58
+ executables = spec.bindir ? collect_files_from_pattern(File.join(@base_dir, spec.bindir, "*")) : []
59
+
60
+ (spec.files + spec.test_files + find_readme_files + executables).uniq
57
61
  end
58
62
 
59
- private
63
+ def error_no_gemspec
64
+ [false, "Error: The specified gemspec file '#{@gemspec_path}' does not exist."]
65
+ end
66
+
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
+ gemspec_dir = Pathname.new(File.dirname(@gemspec_path))
75
+ Dir.glob(gemspec_dir.join("README*")).map do |path|
76
+ Pathname.new(path).relative_path_from(gemspec_dir).to_s
77
+ end
64
78
  end
65
79
 
66
80
  def check_unstaged_files
@@ -71,7 +85,7 @@ module Poepod
71
85
  modified_files = git.status.changed.keys
72
86
 
73
87
  (untracked_files + modified_files).select do |file|
74
- file.start_with?("lib/", "spec/", "test/")
88
+ file.start_with?("bin/", "exe/", "lib/", "spec/", "test/")
75
89
  end
76
90
  rescue Git::GitExecuteError => e
77
91
  warn "Git error: #{e.message}. Assuming no unstaged files."
@@ -1,22 +1,149 @@
1
+ # lib/poepod/processor.rb
1
2
  # frozen_string_literal: true
2
3
 
3
- # lib/poepod/processor.rb
4
+ require "yaml"
5
+ require "base64"
6
+ require "marcel"
7
+ require "stringio"
8
+
4
9
  module Poepod
10
+ # Base processor class
5
11
  class Processor
6
- def initialize(config_file = nil)
12
+ EXCLUDE_DEFAULT = [
13
+ %r{node_modules/}, %r{.git/}, /.gitignore$/, /.DS_Store$/,
14
+ ].freeze
15
+
16
+ def initialize(
17
+ config_file = nil,
18
+ include_binary: false,
19
+ include_dot_files: false,
20
+ exclude: nil,
21
+ base_dir: nil
22
+ )
7
23
  @config = load_config(config_file)
24
+ @include_binary = include_binary
25
+ @include_dot_files = include_dot_files
26
+ @exclude = exclude || EXCLUDE_DEFAULT
27
+ @base_dir = base_dir
28
+ @failed_files = []
29
+ end
30
+
31
+ def process(output_file)
32
+ files_to_process = collect_files_to_process
33
+ total_files, copied_files = process_files(files_to_process, output_file)
34
+ [total_files, copied_files]
35
+ end
36
+
37
+ private
38
+
39
+ def process_files(files, output_file)
40
+ total_files = files.size
41
+ copied_files = 0
42
+
43
+ File.open(output_file, "w", encoding: "utf-8") do |output|
44
+ files.sort.each do |file_path|
45
+ process_file(output, file_path)
46
+ copied_files += 1
47
+ end
48
+ end
49
+
50
+ [total_files, copied_files]
51
+ end
52
+
53
+ def collect_files_to_process
54
+ raise NotImplementedError, "Subclasses must implement collect_files_to_process"
55
+ end
56
+
57
+ def collect_files_from_pattern(pattern)
58
+ expanded_pattern = File.expand_path(pattern)
59
+ if File.directory?(expanded_pattern)
60
+ expanded_pattern = File.join(expanded_pattern, "**", "*")
61
+ end
62
+
63
+ Dir.glob(expanded_pattern, File::FNM_DOTMATCH).each_with_object([]) do |file_path, acc|
64
+ next unless File.file?(file_path)
65
+ next if should_exclude?(file_path)
66
+
67
+ acc << file_path
68
+ end
8
69
  end
9
70
 
10
71
  def load_config(config_file)
11
- if config_file && File.exist?(config_file)
12
- YAML.load_file(config_file)
72
+ return {} unless config_file && File.exist?(config_file)
73
+
74
+ YAML.load_file(config_file)
75
+ end
76
+
77
+ def binary_file?(file_path)
78
+ return false unless File.exist?(file_path) && File.file?(file_path)
79
+
80
+ File.open(file_path, "rb") do |file|
81
+ content = file.read(8192) # Read first 8KB for magic byte detection
82
+ mime_type = Marcel::MimeType.for(
83
+ content,
84
+ name: File.basename(file_path),
85
+ declared_type: "text/plain",
86
+ )
87
+
88
+ !mime_type.start_with?("text/") && mime_type != "application/json"
89
+ end
90
+ end
91
+
92
+ def process_file(output = nil, file_path)
93
+ output ||= StringIO.new
94
+
95
+ relative_path = if @base_dir
96
+ Pathname.new(file_path).relative_path_from(@base_dir).to_s
97
+ else
98
+ file_path
99
+ end
100
+
101
+ puts "Adding to bundle: #{relative_path}"
102
+
103
+ output.puts "--- START FILE: #{relative_path} ---"
104
+
105
+ if binary_file?(file_path) && @include_binary
106
+ output.puts encode_binary_file(file_path)
13
107
  else
14
- {}
108
+ output.puts File.read(file_path)
15
109
  end
110
+
111
+ output.puts "--- END FILE: #{relative_path} ---\n"
112
+
113
+ output.string if output.is_a?(StringIO) # Return the string if using StringIO
114
+ end
115
+
116
+ def encode_binary_file(file_path)
117
+ content = File.binread(file_path)
118
+ mime_type = Marcel::MimeType.for(content, name: File.basename(file_path))
119
+ encoded_content = Base64.strict_encode64(content)
120
+ <<~HERE
121
+ Content-Type: #{mime_type}
122
+ Content-Transfer-Encoding: base64
123
+
124
+ #{encoded_content}
125
+ HERE
126
+ end
127
+
128
+ def dot_file?(file_path)
129
+ File.basename(file_path).start_with?(".")
16
130
  end
17
131
 
18
- def process
19
- raise NotImplementedError, "Subclasses must implement the 'process' method"
132
+ def should_exclude?(file_path)
133
+ return true if !@include_dot_files && dot_file?(file_path)
134
+ return true if !@include_binary && binary_file?(file_path)
135
+
136
+ exclude_file?(file_path)
137
+ end
138
+
139
+ def exclude_file?(file_path)
140
+ @exclude.any? do |pattern|
141
+ if pattern.is_a?(Regexp)
142
+ file_path.match?(pattern)
143
+ else
144
+ File.fnmatch?(pattern, file_path)
145
+ end
146
+ end
20
147
  end
21
148
  end
22
149
  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.7"
5
5
  end
data/poepod.gemspec CHANGED
@@ -18,8 +18,6 @@ Gem::Specification.new do |spec|
18
18
 
19
19
  spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
20
20
 
21
- # Specify which files should be added to the gem when it is released.
22
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
21
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
22
  `git ls-files -z`.split("\x0").reject do |f|
25
23
  f.match(%r{^(test|spec|features)/})
@@ -31,7 +29,7 @@ Gem::Specification.new do |spec|
31
29
  spec.test_files = `git ls-files -- spec/*`.split("\n")
32
30
 
33
31
  spec.add_runtime_dependency "git", "~> 1.11"
34
- spec.add_runtime_dependency "mime-types", "~> 3.3"
32
+ spec.add_runtime_dependency "marcel", "~> 1.0"
35
33
  spec.add_runtime_dependency "parallel", "~> 1.20"
36
34
  spec.add_runtime_dependency "thor", "~> 1.0"
37
35
  spec.add_runtime_dependency "tqdm"
@@ -10,37 +10,61 @@ 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, [text_file], { 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, [text_file, binary_file], { 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
48
+ output_file = File.join(temp_dir, "output.txt")
49
+ expect do
50
+ cli.invoke(:concat, [text_file, dot_file], { 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
39
59
  output_file = File.join(temp_dir, "output.txt")
40
60
  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
61
+ cli.invoke(:concat, [text_file], { output_file: output_file, base_dir: temp_dir })
62
+ end.to output(/1 files detected\.\n.*1 files have been concatenated/).to_stdout
63
+ expect(File.exist?(output_file)).to be true
64
+ content = File.read(output_file)
65
+ expect(content).to include("--- START FILE: text_file.txt ---")
66
+ expect(content).to include("Hello, World!")
67
+ expect(content).to include("--- END FILE: text_file.txt ---")
44
68
  end
45
69
  end
46
70
 
@@ -70,12 +94,10 @@ RSpec.describe Poepod::Cli do
70
94
  end
71
95
 
72
96
  it "wraps a gem" do
97
+ output_file = File.join(temp_dir, "test_gem_wrapped.txt")
73
98
  expect { cli.wrap(gemspec_file) }.to output(/The gem has been wrapped into/).to_stdout
74
- output_file = File.join(Dir.pwd, "test_gem_wrapped.txt")
75
99
  expect(File.exist?(output_file)).to be true
76
100
  content = File.read(output_file)
77
- expect(content).to include("# Wrapped Gem: test_gem")
78
- expect(content).to include("## Gemspec: test_gem.gemspec")
79
101
  expect(content).to include("--- START FILE: lib/test_gem.rb ---")
80
102
  expect(content).to include("puts 'Hello from test_gem'")
81
103
  expect(content).to include("--- END FILE: lib/test_gem.rb ---")
@@ -86,5 +108,18 @@ RSpec.describe Poepod::Cli do
86
108
  cli.wrap("non_existent.gemspec")
87
109
  end.to output(/Error: The specified gemspec file/).to_stdout.and raise_error(SystemExit)
88
110
  end
111
+
112
+ it "uses the specified base directory for relative paths" do
113
+ base_dir = File.dirname(gemspec_file)
114
+ output_file = File.join(base_dir, "test_gem_wrapped.txt")
115
+ expect do
116
+ cli.invoke(:wrap, [gemspec_file], { base_dir: base_dir, output_file: output_file })
117
+ end.to output(/The gem has been wrapped into/).to_stdout
118
+ expect(File.exist?(output_file)).to be true
119
+ content = File.read(output_file)
120
+ expect(content).to include("--- START FILE: lib/test_gem.rb ---")
121
+ expect(content).to include("puts 'Hello from test_gem'")
122
+ expect(content).to include("--- END FILE: lib/test_gem.rb ---")
123
+ end
89
124
  end
90
125
  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([text_file1, text_file2], output_file.path) }
31
+
32
+ it "processes text files and excludes binary and dot files" do
33
+ total_files, copied_files = processor.process(output_file.path)
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(output_file.path)
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(output_file.path)
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(output_file.path)
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
@@ -34,67 +34,78 @@ RSpec.describe Poepod::GemProcessor do
34
34
 
35
35
  describe "#process" do
36
36
  let(:processor) { described_class.new(gemspec_file) }
37
+ let(:output_file) { File.join(temp_dir, "test_gem_wrapped.txt") }
37
38
 
38
39
  before do
39
40
  # Mock Git operations
40
41
  allow(Git).to receive(:open).and_return(double(status: double(untracked: {}, changed: {})))
41
42
  end
42
43
 
43
- it "processes the gem files, includes README files, and spec files" do
44
- success, output_file = processor.process
44
+ it "processes the gem files, includes README files, and spec files in sorted order" do
45
+ success, result, _ = processor.process(output_file)
45
46
  expect(success).to be true
46
47
  expect(File.exist?(output_file)).to be true
47
48
 
48
49
  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 ---")
50
+
51
+ file_order = content.scan(/--- START FILE: (.+) ---/).flatten
52
+ expected_order = [
53
+ "README.md",
54
+ "README.txt",
55
+ "lib/test_gem.rb",
56
+ "spec/test_gem_spec.rb",
57
+ ]
58
+ expect(file_order).to eq(expected_order)
59
+
60
+ expected = <<~HERE
61
+ --- START FILE: README.md ---
62
+ # Test Gem
63
+
64
+ This is a test gem.
65
+ --- END FILE: README.md ---
66
+ --- START FILE: README.txt ---
67
+ Test Gem
68
+
69
+ This is a test gem in plain text.
70
+ --- END FILE: README.txt ---
71
+ --- START FILE: lib/test_gem.rb ---
72
+ puts 'Hello from test_gem'
73
+ --- END FILE: lib/test_gem.rb ---
74
+ --- START FILE: spec/test_gem_spec.rb ---
75
+ RSpec.describe TestGem do
76
+ end
77
+ --- END FILE: spec/test_gem_spec.rb ---
78
+ HERE
79
+ expect(content).to eq(expected)
63
80
  end
64
81
 
65
82
  context "with non-existent gemspec" do
66
83
  let(:processor) { described_class.new("non_existent.gemspec") }
67
84
 
68
85
  it "returns an error" do
69
- success, error_message = processor.process
86
+ success, error_message, _ = processor.process(output_file)
70
87
  expect(success).to be false
71
88
  expect(error_message).to include("Error: The specified gemspec file")
72
89
  end
73
90
  end
74
91
 
75
92
  context "with unstaged files" do
93
+ let(:processor) { described_class.new(gemspec_file, include_unstaged: false) }
76
94
  let(:mock_git) { instance_double(Git::Base) }
77
95
  let(:mock_status) { instance_double(Git::Status) }
78
96
 
79
97
  before do
80
98
  allow(Git).to receive(:open).and_return(mock_git)
81
99
  allow(mock_git).to receive(:status).and_return(mock_status)
82
- allow(mock_status).to receive(:untracked).and_return({ "lib/unstaged_file.rb" => "??" })
100
+ allow(mock_status).to receive(:untracked).and_return(
101
+ { "lib/unstaged_file.rb" => "??" }
102
+ )
83
103
  allow(mock_status).to receive(:changed).and_return({})
84
104
  end
85
105
 
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
106
  context "with include_unstaged option" do
97
- let(:processor) { described_class.new(gemspec_file, nil, true) }
107
+ let(:processor) { described_class.new(gemspec_file, include_unstaged: true) }
108
+ let(:output_file) { File.join(temp_dir, "test_gem_wrapped.txt") }
98
109
 
99
110
  it "includes unstaged files" do
100
111
  allow(File).to receive(:file?).and_return(true)
@@ -105,7 +116,7 @@ RSpec.describe Poepod::GemProcessor do
105
116
  "spec/test_gem_spec.rb" => "RSpec.describe TestGem do\nend",
106
117
  "README.md" => "# Test Gem\n\nThis is a test gem.",
107
118
  "README.txt" => "Test Gem\n\nThis is a test gem in plain text.",
108
- "lib/unstaged_file.rb" => "Unstaged content"
119
+ "lib/unstaged_file.rb" => "Unstaged content",
109
120
  }
110
121
 
111
122
  # Mock File.read
@@ -115,42 +126,46 @@ RSpec.describe Poepod::GemProcessor do
115
126
  file_contents[file_name]
116
127
  elsif path.end_with?("_wrapped.txt")
117
128
  # 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
129
+ file_contents.map do |file, content|
130
+ <<~HERE
131
+ --- START FILE: #{file} ---
132
+ #{content}
133
+ --- END FILE: #{file} ---
134
+ HERE
135
+ end.join("")
129
136
  else
130
137
  "Default content for #{path}"
131
138
  end
132
139
  end
133
140
 
134
- success, output_file, unstaged_files = processor.process
141
+ success, result, unstaged_files = processor.process(output_file)
135
142
  expect(success).to be true
136
143
  expect(unstaged_files).to eq(["lib/unstaged_file.rb"])
137
144
 
138
145
  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 ---")
146
+ expected = <<~HERE
147
+ --- START FILE: lib/test_gem.rb ---
148
+ puts 'Hello from test_gem'
149
+ --- END FILE: lib/test_gem.rb ---
150
+ --- START FILE: spec/test_gem_spec.rb ---
151
+ RSpec.describe TestGem do
152
+ end
153
+ --- END FILE: spec/test_gem_spec.rb ---
154
+ --- START FILE: README.md ---
155
+ # Test Gem
156
+
157
+ This is a test gem.
158
+ --- END FILE: README.md ---
159
+ --- START FILE: README.txt ---
160
+ Test Gem
161
+
162
+ This is a test gem in plain text.
163
+ --- END FILE: README.txt ---
164
+ --- START FILE: lib/unstaged_file.rb ---
165
+ Unstaged content
166
+ --- END FILE: lib/unstaged_file.rb ---
167
+ HERE
168
+ expect(content).to eq(expected)
154
169
  end
155
170
  end
156
171
  end
metadata CHANGED
@@ -1,14 +1,14 @@
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.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-22 00:00:00.000000000 Z
11
+ date: 2024-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: git
@@ -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