From aaa63a9a02a99ccf35a18ce8c5b585dbd295c2a6 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Mon, 22 Jul 2024 21:58:53 +0800 Subject: [PATCH] feat: apply global options to both concat and wrap commands --- README.adoc | 23 ++++-- lib/poepod/cli.rb | 43 ++++++++--- lib/poepod/file_processor.rb | 103 +++++--------------------- lib/poepod/gem_processor.rb | 84 ++++++++++----------- lib/poepod/processor.rb | 115 +++++++++++++++++++++++++++-- poepod.gemspec | 2 +- spec/poepod/cli_spec.rb | 30 +++++++- spec/poepod/file_processor_spec.rb | 65 ++++++++-------- spec/poepod/gem_processor_spec.rb | 110 +++++++++++++-------------- 9 files changed, 329 insertions(+), 246 deletions(-) diff --git a/README.adoc b/README.adoc index fd380eb..9658e08 100644 --- a/README.adoc +++ b/README.adoc @@ -42,6 +42,24 @@ Commands: poepod wrap GEMSPEC_PATH # Wrap a gem based on its gemspec file ---- +=== Global options + +All options can be used for both `wrap` and `concat` commands: + +* `--exclude`: List of patterns to exclude (default: `["node_modules/", ".git/", ".gitignore$", ".DS_Store$", "^\\..+"]`) +* `--config`: Path to configuration file +* `--include-binary`: Include binary files (encoded in MIME format) +* `--include-dot-files`: Include dot files +* `--output-file`: Output path +* `--base-dir`: Base directory for relative file paths in output +* `--include-unstaged`: Include unstaged files from `lib`, `spec`, and `test` directories (for `wrap` command only) + +[source,shell] +---- +$ poepod concat FILES [OUTPUT_FILE] --exclude PATTERNS --config PATH --include-binary --include-dot-files --output-file PATH --base-dir PATH +$ poepod wrap GEMSPEC_PATH --exclude PATTERNS --config PATH --include-binary --include-dot-files --output-file PATH --base-dir PATH --include-unstaged +---- + === Concatenating files The `concat` command allows you to combine multiple files into a single text @@ -54,7 +72,6 @@ coding assistants. By default, it excludes binary files, dot files, and certain patterns like `node_modules/` and `.git/`. - [source,shell] ---- $ poepod concat path/to/files/* output.txt @@ -63,7 +80,6 @@ $ poepod concat path/to/files/* output.txt This will concatenate all non-binary, non-dot files from the specified path into `output.txt`. - ==== Including dot files By default, dot files (hidden files starting with a dot) are excluded. @@ -75,7 +91,6 @@ To include them, use the `--include-dot-files` option: $ poepod concat path/to/files/* output.txt --include-dot-files ---- - ==== Including binary files By default, binary files are excluded to keep the output focused on readable @@ -92,7 +107,6 @@ $ poepod concat path/to/files/* output.txt --include-binary This can be useful when you need to include binary assets or compiled files in your analysis. - ==== Excluding patterns You can exclude certain patterns using the `--exclude` option: @@ -105,7 +119,6 @@ $ poepod concat path/to/files/* output.txt --exclude node_modules .git build tes This is helpful when you want to focus on specific parts of your codebase, excluding irrelevant or large directories. - === Wrapping a gem The `wrap` command creates a comprehensive snapshot of your gem, including all diff --git a/lib/poepod/cli.rb b/lib/poepod/cli.rb index 9772a71..f6b9f7d 100644 --- a/lib/poepod/cli.rb +++ b/lib/poepod/cli.rb @@ -8,31 +8,50 @@ module Poepod # Command-line interface for Poepod class Cli < Thor + # Define shared options + def self.shared_options + option :exclude, type: :array, default: Poepod::FileProcessor::EXCLUDE_DEFAULT, + desc: "List of patterns to exclude" + option :config, type: :string, desc: "Path to configuration file" + option :include_binary, type: :boolean, default: false, desc: "Include binary files (encoded in MIME format)" + option :include_dot_files, type: :boolean, default: false, desc: "Include dot files" + option :output_file, type: :string, desc: "Output path" + option :base_dir, type: :string, desc: "Base directory for relative file paths in output" + end + desc "concat FILES [OUTPUT_FILE]", "Concatenate specified files into one text file" - option :exclude, type: :array, default: Poepod::FileProcessor::EXCLUDE_DEFAULT, desc: "List of patterns to exclude" - option :config, type: :string, desc: "Path to configuration file" - option :include_binary, type: :boolean, default: false, desc: "Include binary files (encoded in MIME format)" - option :include_dot_files, type: :boolean, default: false, desc: "Include dot files" - option :output_file, type: :string, desc: "Output path" + shared_options def concat(*files) check_files(files) output_file = determine_output_file(files) - process_files(files, output_file) + base_dir = options[:base_dir] || Dir.pwd + process_files(files, output_file, base_dir) end desc "wrap GEMSPEC_PATH", "Wrap a gem based on its gemspec file" + shared_options option :include_unstaged, type: :boolean, default: false, desc: "Include unstaged files from lib, spec, and test directories" def wrap(gemspec_path) + base_dir = options[:base_dir] || File.dirname(gemspec_path) processor = Poepod::GemProcessor.new( gemspec_path, - nil, - include_unstaged: options[:include_unstaged] + include_unstaged: options[:include_unstaged], + exclude: options[:exclude], + include_binary: options[:include_binary], + include_dot_files: options[:include_dot_files], + base_dir: base_dir, + config_file: options[:config] ) success, result, unstaged_files = processor.process - handle_wrap_result(success, result, unstaged_files) + if success + handle_wrap_result(success, result, unstaged_files) + else + puts result + exit(1) + end end def self.exit_on_failure? @@ -52,14 +71,16 @@ def determine_output_file(files) options[:output_file] || default_output_file(files.first) end - def process_files(files, output_file) + def process_files(files, output_file, base_dir) output_path = Pathname.new(output_file).expand_path processor = Poepod::FileProcessor.new( files, output_path, config_file: options[:config], include_binary: options[:include_binary], - include_dot_files: options[:include_dot_files] + include_dot_files: options[:include_dot_files], + exclude: options[:exclude], + base_dir: base_dir ) total_files, copied_files = processor.process print_result(total_files, copied_files, output_path) diff --git a/lib/poepod/file_processor.rb b/lib/poepod/file_processor.rb index 8dab074..79dd985 100644 --- a/lib/poepod/file_processor.rb +++ b/lib/poepod/file_processor.rb @@ -1,43 +1,32 @@ # frozen_string_literal: true require_relative "processor" -require "yaml" -require "tqdm" -require "pathname" -require "open3" -require "base64" -require "mime/types" module Poepod # Processes files for concatenation, handling binary and dot files class FileProcessor < Processor EXCLUDE_DEFAULT = [ - %r{node_modules/}, %r{.git/}, /.gitignore$/, /.DS_Store$/, /^\..+/ # Add dot files pattern + %r{node_modules/}, %r{.git/}, /.gitignore$/, /.DS_Store$/, /^\..+/ ].freeze - def initialize(files, output_file, config_file: nil, include_binary: false, include_dot_files: false) - super(config_file) + def initialize( + files, + output_file, + config_file: nil, + include_binary: false, + include_dot_files: false, + exclude: [], + base_dir: nil + ) + super( + config_file, + include_binary: include_binary, + include_dot_files: include_dot_files, + exclude: exclude, + base_dir: base_dir, + ) @files = files @output_file = output_file - @failed_files = [] - @include_binary = include_binary - @include_dot_files = include_dot_files - end - - def process - _ = 0 - copied_files = 0 - files_to_process = collect_files_to_process - total_files = files_to_process.size - - File.open(@output_file, "w", encoding: "utf-8") do |output| - files_to_process.sort.each do |file_path| - process_single_file(file_path, output) - copied_files += 1 - end - end - - [total_files, copied_files] end private @@ -46,67 +35,11 @@ def collect_files_to_process @files.flatten.each_with_object([]) do |file, files_to_process| Dir.glob(file, File::FNM_DOTMATCH).each do |matched_file| next unless File.file?(matched_file) - next if dot_file?(matched_file) && !@include_dot_files - next if binary_file?(matched_file) && !@include_binary + next if should_exclude?(matched_file) files_to_process << matched_file end end end - - def process_single_file(file_path, output) - file_path, content, error = process_file(file_path) - if content - output.puts "--- START FILE: #{file_path} ---" - output.puts content - output.puts "--- END FILE: #{file_path} ---" - elsif error - warn "ERROR: #{file_path}: #{error}" - end - end - - def dot_file?(file_path) - File.basename(file_path).start_with?(".") - end - - def binary_file?(file_path) - !text_file?(file_path) - end - - def process_file(file_path) - if text_file?(file_path) - process_text_file(file_path) - elsif @include_binary - process_binary_file(file_path) - else - [file_path, nil, nil] # Skipped binary file - end - rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError - handle_encoding_error(file_path) - end - - def process_text_file(file_path) - [file_path, File.read(file_path, encoding: "utf-8"), nil] - end - - def process_binary_file(file_path) - [file_path, encode_binary_file(file_path), nil] - end - - def handle_encoding_error(file_path) - @failed_files << file_path - [file_path, nil, "Failed to decode the file, as it is not saved with UTF-8 encoding."] - end - - def text_file?(file_path) - stdout, status = Open3.capture2("file", "-b", "--mime-type", file_path) - status.success? && stdout.strip.start_with?("text/") - end - - def encode_binary_file(file_path) - mime_type = MIME::Types.type_for(file_path).first.content_type - encoded_content = Base64.strict_encode64(File.binread(file_path)) - "Content-Type: #{mime_type}\nContent-Transfer-Encoding: base64\n\n#{encoded_content}" - end end end diff --git a/lib/poepod/gem_processor.rb b/lib/poepod/gem_processor.rb index 8cb771c..649db00 100644 --- a/lib/poepod/gem_processor.rb +++ b/lib/poepod/gem_processor.rb @@ -8,8 +8,22 @@ module Poepod # Processes gem files for wrapping, handling unstaged files class GemProcessor < Processor - def initialize(gemspec_path, config_file = nil, include_unstaged: false) - super(config_file) + def initialize( + gemspec_path, + include_unstaged: false, + exclude: [], + include_binary: false, + include_dot_files: false, + base_dir: nil, + config_file: nil + ) + super( + config_file, + include_binary: include_binary, + include_dot_files: include_dot_files, + exclude: exclude, + base_dir: base_dir || File.dirname(gemspec_path), + ) @gemspec_path = gemspec_path @include_unstaged = include_unstaged end @@ -21,16 +35,31 @@ def process return spec unless spec.is_a?(Gem::Specification) gem_name = spec.name - output_file = "#{gem_name}_wrapped.txt" + @output_file = "#{gem_name}_wrapped.txt" unstaged_files = check_unstaged_files - write_wrapped_gem(spec, output_file, unstaged_files) + super() - [true, output_file, unstaged_files] + [true, @output_file, unstaged_files] end private + def collect_files_to_process + spec = load_gemspec + files_to_include = (spec.files + + spec.test_files + + find_readme_files).uniq + + files_to_include += check_unstaged_files if @include_unstaged + + files_to_include.sort.uniq.reject do |relative_path| + should_exclude?(File.join(@base_dir, relative_path)) + end.map do |relative_path| + File.join(@base_dir, relative_path) + end + end + def error_no_gemspec [false, "Error: The specified gemspec file '#{@gemspec_path}' does not exist."] end @@ -41,47 +70,12 @@ def load_gemspec [false, "Error loading gemspec: #{e.message}"] end - def write_wrapped_gem(spec, output_file, unstaged_files) - File.open(output_file, "w") do |file| - write_header(file, spec) - write_unstaged_warning(file, unstaged_files) if unstaged_files.any? - write_files_content(file, spec, unstaged_files) - end - end - - def write_header(file, spec) - file.puts "# Wrapped Gem: #{spec.name}" - file.puts "## Gemspec: #{File.basename(@gemspec_path)}" - end - - def write_unstaged_warning(file, unstaged_files) - file.puts "\n## Warning: Unstaged Files" - file.puts unstaged_files.sort.join("\n") - file.puts "\nThese files are not included in the wrap unless --include-unstaged option is used." - end - - def write_files_content(file, spec, unstaged_files) - file.puts "\n## Files:\n" - files_to_include = (spec.files + spec.test_files + find_readme_files).uniq - files_to_include += unstaged_files if @include_unstaged - - files_to_include.sort.uniq.each do |relative_path| - write_file_content(file, relative_path) - end - end - - def write_file_content(file, relative_path) - full_path = File.join(File.dirname(@gemspec_path), relative_path) - return unless File.file?(full_path) - - file.puts "--- START FILE: #{relative_path} ---" - file.puts File.read(full_path) - file.puts "--- END FILE: #{relative_path} ---\n\n" - end - def find_readme_files - Dir.glob(File.join(File.dirname(@gemspec_path), "README*")) - .map { |path| Pathname.new(path).relative_path_from(Pathname.new(File.dirname(@gemspec_path))).to_s } + Dir.glob(File.join(File.dirname(@gemspec_path), "README*")).map do |path| + Pathname.new(path).relative_path_from( + Pathname.new(File.dirname(@gemspec_path)) + ).to_s + end end def check_unstaged_files diff --git a/lib/poepod/processor.rb b/lib/poepod/processor.rb index 7f5478f..84ed0a3 100644 --- a/lib/poepod/processor.rb +++ b/lib/poepod/processor.rb @@ -1,22 +1,123 @@ # frozen_string_literal: true +require "yaml" +require "base64" +require "marcel" +require "stringio" + module Poepod - # Base class for file processors + # Base processor class class Processor - def initialize(config_file = nil) + def initialize( + config_file = nil, + include_binary: false, + include_dot_files: false, + exclude: [], + base_dir: nil + ) @config = load_config(config_file) + @include_binary = include_binary + @include_dot_files = include_dot_files + @exclude = exclude || [] + @base_dir = base_dir + @failed_files = [] + end + + def process + files_to_process = collect_files_to_process + total_files, copied_files = process_files(files_to_process) + [total_files, copied_files] + end + + private + + def collect_files_to_process + raise NotImplementedError, "Subclasses must implement collect_files_to_process" end def load_config(config_file) - if config_file && File.exist?(config_file) - YAML.load_file(config_file) + return {} unless config_file && File.exist?(config_file) + + YAML.load_file(config_file) + end + + def binary_file?(file_path) + return false unless File.exist?(file_path) && File.file?(file_path) + + File.open(file_path, "rb") do |file| + content = file.read(8192) # Read first 8KB for magic byte detection + mime_type = Marcel::MimeType.for(content, name: File.basename(file_path), declared_type: "text/plain") + !mime_type.start_with?("text/") && mime_type != "application/json" + end + end + + def process_files(files) + total_files = files.size + copied_files = 0 + + File.open(@output_file, "w", encoding: "utf-8") do |output| + files.sort.each do |file_path| + process_file(output, file_path) + copied_files += 1 + end + end + + [total_files, copied_files] + end + + def process_file(output = nil, file_path) + output ||= StringIO.new + + relative_path = if @base_dir + Pathname.new(file_path).relative_path_from(Pathname.new(@base_dir)).to_s + else + file_path + end + + output.puts "--- START FILE: #{relative_path} ---" + + if binary_file?(file_path) && @include_binary + output.puts encode_binary_file(file_path) else - {} + output.puts File.read(file_path) end + + output.puts "--- END FILE: #{relative_path} ---\n" + + output.string if output.is_a?(StringIO) # Return the string if using StringIO end - def process - raise NotImplementedError, "Subclasses must implement the 'process' method" + def encode_binary_file(file_path) + content = File.binread(file_path) + mime_type = Marcel::MimeType.for(content, name: File.basename(file_path)) + encoded_content = Base64.strict_encode64(content) + <<~HERE + Content-Type: #{mime_type} + Content-Transfer-Encoding: base64 + + #{encoded_content} + HERE + end + + def dot_file?(file_path) + File.basename(file_path).start_with?(".") + end + + def should_exclude?(file_path) + return true if !@include_dot_files && dot_file?(file_path) + return true if !@include_binary && binary_file?(file_path) + + exclude_file?(file_path) + end + + def exclude_file?(file_path) + @exclude.any? do |pattern| + if pattern.is_a?(Regexp) + file_path.match?(pattern) + else + File.fnmatch?(pattern, file_path) + end + end end end end diff --git a/poepod.gemspec b/poepod.gemspec index 694f68a..df05be3 100644 --- a/poepod.gemspec +++ b/poepod.gemspec @@ -31,7 +31,7 @@ Gem::Specification.new do |spec| spec.test_files = `git ls-files -- spec/*`.split("\n") spec.add_runtime_dependency "git", "~> 1.11" - spec.add_runtime_dependency "mime-types", "~> 3.3" + spec.add_runtime_dependency "marcel", "~> 1.0" spec.add_runtime_dependency "parallel", "~> 1.20" spec.add_runtime_dependency "thor", "~> 1.0" spec.add_runtime_dependency "tqdm" diff --git a/spec/poepod/cli_spec.rb b/spec/poepod/cli_spec.rb index 9841151..b7829db 100644 --- a/spec/poepod/cli_spec.rb +++ b/spec/poepod/cli_spec.rb @@ -41,7 +41,7 @@ expect(File.exist?(output_file)).to be true content = File.read(output_file) expect(content).to include("Hello, World!") - expect(content).to include("Content-Type: application/octet-stream") + expect(content).to include("Content-Type: image/jpeg") end it "includes dot files when specified" do @@ -54,6 +54,19 @@ expect(content).to include("Hello, World!") expect(content).to include("Hidden content") end + + it "uses the specified base directory for relative paths" do + output_file = File.join(temp_dir, "output.txt") + base_dir = File.dirname(text_file) + expect do + cli.invoke(:concat, [File.join(temp_dir, "*")], { output_file: output_file, base_dir: base_dir }) + end.to output(/1 files detected\.\n.*1 files have been concatenated/).to_stdout + expect(File.exist?(output_file)).to be true + content = File.read(output_file) + expect(content).to include("--- START FILE: text_file.txt ---") + expect(content).to include("Hello, World!") + expect(content).to include("--- END FILE: text_file.txt ---") + end end describe "#wrap" do @@ -86,8 +99,6 @@ output_file = File.join(Dir.pwd, "test_gem_wrapped.txt") expect(File.exist?(output_file)).to be true content = File.read(output_file) - expect(content).to include("# Wrapped Gem: test_gem") - expect(content).to include("## Gemspec: test_gem.gemspec") expect(content).to include("--- START FILE: lib/test_gem.rb ---") expect(content).to include("puts 'Hello from test_gem'") expect(content).to include("--- END FILE: lib/test_gem.rb ---") @@ -98,5 +109,18 @@ cli.wrap("non_existent.gemspec") end.to output(/Error: The specified gemspec file/).to_stdout.and raise_error(SystemExit) end + + it "uses the specified base directory for relative paths" do + base_dir = File.dirname(gemspec_file) + expect do + cli.invoke(:wrap, [gemspec_file], { base_dir: base_dir }) + end.to output(/The gem has been wrapped into/).to_stdout + output_file = File.join(Dir.pwd, "test_gem_wrapped.txt") + expect(File.exist?(output_file)).to be true + content = File.read(output_file) + expect(content).to include("--- START FILE: lib/test_gem.rb ---") + expect(content).to include("puts 'Hello from test_gem'") + expect(content).to include("--- END FILE: lib/test_gem.rb ---") + end end end diff --git a/spec/poepod/file_processor_spec.rb b/spec/poepod/file_processor_spec.rb index f9a04da..487d477 100644 --- a/spec/poepod/file_processor_spec.rb +++ b/spec/poepod/file_processor_spec.rb @@ -31,8 +31,8 @@ it "processes text files and excludes binary and dot files" do total_files, copied_files = processor.process - expect(total_files).to eq(2) # Only text files are detected - expect(copied_files).to eq(2) # Only text files are processed + expect(total_files).to eq(2) + expect(copied_files).to eq(2) output_content = File.read(output_file.path, encoding: "utf-8") expected_content = <<~TEXT @@ -52,13 +52,13 @@ it "includes binary files" do total_files, copied_files = processor.process - expect(total_files).to eq(3) # Text files and binary file - expect(copied_files).to eq(3) # Text files and binary file + expect(total_files).to eq(3) + expect(copied_files).to eq(3) output_content = File.read(output_file.path, encoding: "utf-8") expected_content = <<~TEXT --- START FILE: #{binary_file} --- - Content-Type: application/octet-stream + Content-Type: image/jpeg Content-Transfer-Encoding: base64 /9j/4A== @@ -79,8 +79,8 @@ it "includes dot files" do total_files, copied_files = processor.process - expect(total_files).to eq(3) # Text files and dot file - expect(copied_files).to eq(3) # Text files and dot file + expect(total_files).to eq(3) + expect(copied_files).to eq(3) output_content = File.read(output_file.path, encoding: "utf-8") expected_content = <<~TEXT @@ -105,27 +105,27 @@ it "includes all files in sorted order" do total_files, copied_files = processor.process - expect(total_files).to eq(4) # All files - expect(copied_files).to eq(4) # All files + expect(total_files).to eq(4) + expect(copied_files).to eq(4) output_content = File.read(output_file.path, encoding: "utf-8") - expected_content = [ - "--- START FILE: #{dot_file} ---", - "Content of hidden file.", - "--- END FILE: #{dot_file} ---", - "--- START FILE: #{binary_file} ---", - "Content-Type: application/octet-stream", - "Content-Transfer-Encoding: base64", - "", - "/9j/4A==", - "--- END FILE: #{binary_file} ---", - "--- START FILE: #{text_file1} ---", - "Content of file1.", - "--- END FILE: #{text_file1} ---", - "--- START FILE: #{text_file2} ---", - "Content of file2.", - "--- END FILE: #{text_file2} ---" - ].join("\n") + "\n" + expected_content = <<~HERE + --- START FILE: #{dot_file} --- + Content of hidden file. + --- END FILE: #{dot_file} --- + --- START FILE: #{binary_file} --- + Content-Type: image/jpeg + Content-Transfer-Encoding: base64 + + /9j/4A== + --- END FILE: #{binary_file} --- + --- START FILE: #{text_file1} --- + Content of file1. + --- END FILE: #{text_file1} --- + --- START FILE: #{text_file2} --- + Content of file2. + --- END FILE: #{text_file2} --- + HERE expect(output_content).to eq(expected_content) end @@ -136,18 +136,15 @@ let(:processor) { described_class.new([text_file1], output_file.path) } it "reads the content of a file" do - file_path, content, error = processor.send(:process_file, text_file1) - expect(file_path).to eq(text_file1) - expect(content).to eq("Content of file1.\n") - expect(error).to be_nil + content = processor.send(:process_file, nil, text_file1) + expect(content).to include("Content of file1.\n") end it "handles encoding errors gracefully" do allow(File).to receive(:read).and_raise(Encoding::InvalidByteSequenceError) - file_path, content, error = processor.send(:process_file, text_file1) - expect(file_path).to eq(text_file1) - expect(content).to be_nil - expect(error).to eq("Failed to decode the file, as it is not saved with UTF-8 encoding.") + expect do + processor.send(:process_file, nil, text_file1) + end.to raise_error(Encoding::InvalidByteSequenceError) end end end diff --git a/spec/poepod/gem_processor_spec.rb b/spec/poepod/gem_processor_spec.rb index 93cf908..4a063fc 100644 --- a/spec/poepod/gem_processor_spec.rb +++ b/spec/poepod/gem_processor_spec.rb @@ -46,8 +46,6 @@ expect(File.exist?(output_file)).to be true content = File.read(output_file) - expect(content).to include("# Wrapped Gem: test_gem") - expect(content).to include("## Gemspec: test_gem.gemspec") file_order = content.scan(/--- START FILE: (.+) ---/).flatten expected_order = [ @@ -58,20 +56,26 @@ ] expect(file_order).to eq(expected_order) - expect(content).to include("# Wrapped Gem: test_gem") - expect(content).to include("## Gemspec: test_gem.gemspec") - expect(content).to include("--- START FILE: lib/test_gem.rb ---") - expect(content).to include("puts 'Hello from test_gem'") - expect(content).to include("--- END FILE: lib/test_gem.rb ---") - expect(content).to include("--- START FILE: spec/test_gem_spec.rb ---") - expect(content).to include("RSpec.describe TestGem do") - expect(content).to include("--- END FILE: spec/test_gem_spec.rb ---") - expect(content).to include("--- START FILE: README.md ---") - expect(content).to include("# Test Gem\n\nThis is a test gem.") - expect(content).to include("--- END FILE: README.md ---") - expect(content).to include("--- START FILE: README.txt ---") - expect(content).to include("Test Gem\n\nThis is a test gem in plain text.") - expect(content).to include("--- END FILE: README.txt ---") + expected = <<~HERE + --- START FILE: README.md --- + # Test Gem + + This is a test gem. + --- END FILE: README.md --- + --- START FILE: README.txt --- + Test Gem + + This is a test gem in plain text. + --- END FILE: README.txt --- + --- START FILE: lib/test_gem.rb --- + puts 'Hello from test_gem' + --- END FILE: lib/test_gem.rb --- + --- START FILE: spec/test_gem_spec.rb --- + RSpec.describe TestGem do + end + --- END FILE: spec/test_gem_spec.rb --- + HERE + expect(content).to eq(expected) end context "with non-existent gemspec" do @@ -85,29 +89,21 @@ end context "with unstaged files" do - let(:processor) { described_class.new(gemspec_file, nil, include_unstaged: true) } + let(:processor) { described_class.new(gemspec_file, include_unstaged: false) } let(:mock_git) { instance_double(Git::Base) } let(:mock_status) { instance_double(Git::Status) } before do allow(Git).to receive(:open).and_return(mock_git) allow(mock_git).to receive(:status).and_return(mock_status) - allow(mock_status).to receive(:untracked).and_return({ "lib/unstaged_file.rb" => "??" }) + allow(mock_status).to receive(:untracked).and_return( + { "lib/unstaged_file.rb" => "??" } + ) allow(mock_status).to receive(:changed).and_return({}) end - it "warns about unstaged files" do - success, output_file, unstaged_files = processor.process - expect(success).to be true - expect(unstaged_files).to eq(["lib/unstaged_file.rb"]) - - content = File.read(output_file) - expect(content).to include("## Warning: Unstaged Files") - expect(content).to include("lib/unstaged_file.rb") - end - context "with include_unstaged option" do - let(:processor) { described_class.new(gemspec_file, nil, include_unstaged: true) } + let(:processor) { described_class.new(gemspec_file, include_unstaged: true) } it "includes unstaged files" do allow(File).to receive(:file?).and_return(true) @@ -128,17 +124,13 @@ file_contents[file_name] elsif path.end_with?("_wrapped.txt") # This is the output file, so we'll construct its content here - wrapped_content = "# Wrapped Gem: test_gem\n" - wrapped_content += "## Gemspec: test_gem.gemspec\n\n" - wrapped_content += "## Warning: Unstaged Files\n" - wrapped_content += "lib/unstaged_file.rb\n\n" - wrapped_content += "## Files:\n\n" - file_contents.each do |file, content| - wrapped_content += "--- START FILE: #{file} ---\n" - wrapped_content += "#{content}\n" - wrapped_content += "--- END FILE: #{file} ---\n\n" - end - wrapped_content + file_contents.map do |file, content| + <<~HERE + --- START FILE: #{file} --- + #{content} + --- END FILE: #{file} --- + HERE + end.join("") else "Default content for #{path}" end @@ -149,21 +141,29 @@ expect(unstaged_files).to eq(["lib/unstaged_file.rb"]) content = File.read(output_file) - expect(content).to include("--- START FILE: lib/test_gem.rb ---") - expect(content).to include("puts 'Hello from test_gem'") - expect(content).to include("--- END FILE: lib/test_gem.rb ---") - expect(content).to include("--- START FILE: spec/test_gem_spec.rb ---") - expect(content).to include("RSpec.describe TestGem do") - expect(content).to include("--- END FILE: spec/test_gem_spec.rb ---") - expect(content).to include("--- START FILE: README.md ---") - expect(content).to include("# Test Gem\n\nThis is a test gem.") - expect(content).to include("--- END FILE: README.md ---") - expect(content).to include("--- START FILE: README.txt ---") - expect(content).to include("Test Gem\n\nThis is a test gem in plain text.") - expect(content).to include("--- END FILE: README.txt ---") - expect(content).to include("--- START FILE: lib/unstaged_file.rb ---") - expect(content).to include("Unstaged content") - expect(content).to include("--- END FILE: lib/unstaged_file.rb ---") + expected = <<~HERE + --- START FILE: lib/test_gem.rb --- + puts 'Hello from test_gem' + --- END FILE: lib/test_gem.rb --- + --- START FILE: spec/test_gem_spec.rb --- + RSpec.describe TestGem do + end + --- END FILE: spec/test_gem_spec.rb --- + --- START FILE: README.md --- + # Test Gem + + This is a test gem. + --- END FILE: README.md --- + --- START FILE: README.txt --- + Test Gem + + This is a test gem in plain text. + --- END FILE: README.txt --- + --- START FILE: lib/unstaged_file.rb --- + Unstaged content + --- END FILE: lib/unstaged_file.rb --- + HERE + expect(content).to eq(expected) end end end