diff --git a/README.adoc b/README.adoc index c17e23a..ef6f3d1 100644 --- a/README.adoc +++ b/README.adoc @@ -1,7 +1,10 @@ = Poepod -Poepod is a Ruby gem that provides functionality to concatenate code files from -a directory into one text file for analysis by Poe. +Poepod is a Ruby gem that streamlines the process of preparing code for analysis +by Poe. It offers two main features: concatenating multiple files into a single +text file, and wrapping gem contents including unstaged files. These features +are particularly useful for developers who want to quickly gather code for +review, analysis, or submission to AI-powered coding assistants. == Installation @@ -28,49 +31,102 @@ $ gem install poepod == Usage -After installation, you can use the `poepod` command line tool to concatenate -code files: +After installation, you can use the `poepod` command line tool: [source,shell] ---- $ poepod help Commands: - poepod concat DIRECTORY OUTPUT_FILE # Concatenate code from a directory into one text file - poepod help [COMMAND] # Describe available commands or one specific command + poepod concat FILES [OUTPUT_FILE] # Concatenate specified files into one text file + poepod help [COMMAND] # Describe available commands or one specific command + poepod wrap GEMSPEC_PATH # Wrap a gem based on its gemspec file +---- + +=== Concatenating files + +The `concat` command allows you to combine multiple files into a single text +file. This is particularly useful when you want to review or analyze code from +multiple files in one place, or when preparing code submissions for AI-powered +coding assistants. + +[source,shell] +---- +$ poepod concat path/to/files/* output.txt +---- + +This will concatenate all files from the specified path into `output.txt`. + +==== Excluding patterns + +You can exclude certain patterns using the `--exclude` option: + +[source,shell] +---- +$ poepod concat path/to/files/* output.txt --exclude node_modules .git build test +---- + +This is helpful when you want to focus on specific parts of your codebase, +excluding irrelevant or large directories. -$ poepod help concat -Usage: - poepod concat DIRECTORY OUTPUT_FILE +==== Including binary files -Options: - [--exclude=one two three] # List of patterns to exclude - # Default: "node_modules/" ".git/" "build" "test" ".gitignore" ".DS_Store" "*.jpg" "*.jpeg" "*.png" "*.svg" "*.gif" "*.exe" "*.dll" "*.so" "*.bin" "*.o" "*.a" - [--config=CONFIG] # Path to configuration file +By default, binary files are excluded to keep the output focused on readable +code. However, you can include binary files (encoded in MIME format) using the +`--include-binary` option: -Concatenate code from a directory into one text file +[source,shell] +---- +$ poepod concat path/to/files/* output.txt --include-binary ---- -For example: +This can be useful when you need to include binary assets or compiled files in +your analysis. + +=== Wrapping a gem + +The `wrap` command creates a comprehensive snapshot of your gem, including all +files specified in the gemspec and README files. This is particularly useful for +gem developers who want to review their entire gem contents or prepare it for +submission to code review tools. [source,shell] ---- -$ poepod concat my_project -# => concatenated into my_project.txt +$ poepod wrap path/to/your_gem.gemspec ---- -This will concatenate all code files from the specified directory into `output.txt`. +This will create a file named `your_gem_wrapped.txt` containing all the files +specified in the gemspec, including README files. + +==== Handling unstaged files -You can also exclude certain directories or files by using the `--exclude` option: +By default, unstaged files in the `lib/`, `spec/`, and `test/` directories are +not included in the wrap, but they will be listed as a warning. This default +behavior ensures that the wrapped content matches what's currently tracked in +your version control system. + +However, there are cases where including unstaged files can be beneficial: + +. When you're actively developing and want to include recent changes that +haven't been committed yet. + +. When you're seeking feedback on work-in-progress code. + +. When you want to ensure you're not missing any important files in your commit. + +To include these unstaged files in the wrap: [source,shell] ---- -$ poepod concat my_project output.txt --exclude node_modules .git build test .gitignore .DS_Store .jpg .png .svg +$ poepod wrap path/to/your_gem.gemspec --include-unstaged ---- +This option allows you to capture a true snapshot of your gem's current state, +including any work in progress. + == Development After checking out the repo, run `bin/setup` to install dependencies. Then, run -`rake test` to run the tests. You can also run `bin/console` for an interactive +`rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. To install this gem onto your local machine, run `bundle exec rake install`. To @@ -81,4 +137,5 @@ https://rubygems.org. == Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/riboseinc/poepod. \ No newline at end of file +Bug reports and pull requests are welcome on GitHub at https://github.com/riboseinc/poepod. +Please adhere to the link:CODE_OF_CONDUCT.md[code of conduct]. \ No newline at end of file diff --git a/lib/poepod/cli.rb b/lib/poepod/cli.rb index d8fa27f..678517d 100644 --- a/lib/poepod/cli.rb +++ b/lib/poepod/cli.rb @@ -1,36 +1,69 @@ -# frozen_string_literal: true - +# lib/poepod/cli.rb require "thor" -require_relative "processor" +require_relative "file_processor" +require_relative "gem_processor" module Poepod class Cli < Thor - desc "concat DIRECTORY OUTPUT_FILE", "Concatenate code from a directory into one text file" - option :exclude, type: :array, default: Poepod::Processor::EXCLUDE_DEFAULT, desc: "List of patterns to exclude" + 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)" - def concat(directory, output_file = nil) - dir_path = Pathname.new(directory) - - # Check if the directory exists - unless dir_path.directory? - puts "Error: Directory '#{directory}' does not exist." + def concat(*files, output_file: nil) + if files.empty? + puts "Error: No files specified." exit(1) end - dir_path = dir_path.expand_path unless dir_path.absolute? + output_file ||= default_output_file(files.first) + output_path = Pathname.new(output_file).expand_path + + processor = Poepod::FileProcessor.new(files, output_path, options[:config], options[:include_binary]) + total_files, copied_files = processor.process + + puts "-> #{total_files} files detected." + puts "=> #{copied_files} files have been concatenated into #{output_path.relative_path_from(Dir.pwd)}." + end + + desc "wrap GEMSPEC_PATH", "Wrap a gem based on its gemspec file" + option :include_unstaged, type: :boolean, default: false, desc: "Include unstaged files from lib, spec, and test directories" - output_file ||= "#{dir_path.basename}.txt" - output_path = dir_path.dirname.join(output_file) - processor = Poepod::Processor.new(options[:config]) - total_files, copied_files = processor.write_directory_structure_to_file(directory, output_path, options[:exclude]) + def wrap(gemspec_path) + processor = Poepod::GemProcessor.new(gemspec_path, nil, options[:include_unstaged]) + success, result, unstaged_files = processor.process - puts "-> #{total_files} files detected in the #{dir_path.relative_path_from(Dir.pwd)} directory." - puts "=> #{copied_files} files have been concatenated into #{Pathname.new(output_path).relative_path_from(Dir.pwd)}." + if success + puts "=> The gem has been wrapped into '#{result}'." + if unstaged_files.any? + puts "\nWarning: The following files are not staged in git:" + puts unstaged_files + puts "\nThese files are #{options[:include_unstaged] ? "included" : "not included"} in the wrap." + puts "Use --include-unstaged option to include these files." unless options[:include_unstaged] + end + else + puts result + exit(1) + end end def self.exit_on_failure? true end + + private + + def default_output_file(first_pattern) + first_item = Dir.glob(first_pattern).first + if first_item + if File.directory?(first_item) + "#{File.basename(first_item)}.txt" + else + "#{File.basename(first_item, ".*")}_concat.txt" + end + else + "concatenated_output.txt" + end + end end end diff --git a/lib/poepod/file_processor.rb b/lib/poepod/file_processor.rb new file mode 100644 index 0000000..bb0d4ec --- /dev/null +++ b/lib/poepod/file_processor.rb @@ -0,0 +1,77 @@ +require_relative "processor" +require "yaml" +require "tqdm" +require "pathname" +require "open3" +require "base64" +require "mime/types" + +module Poepod + class FileProcessor < Processor + EXCLUDE_DEFAULT = [ + /node_modules\//, /.git\//, /.gitignore$/, /.DS_Store$/, + ].freeze + + def initialize(files, output_file, config_file = nil, include_binary = false) + super(config_file) + @files = files + @output_file = output_file + @failed_files = [] + @include_binary = include_binary + end + + def process + total_files = 0 + copied_files = 0 + + File.open(@output_file, "w", encoding: "utf-8") do |output| + @files.each do |file| + Dir.glob(file).each do |matched_file| + if File.file?(matched_file) + total_files += 1 + file_path, content, error = process_file(matched_file) + if content + output.puts "--- START FILE: #{file_path} ---" + output.puts content + output.puts "--- END FILE: #{file_path} ---" + copied_files += 1 + elsif error + output.puts "#{file_path}\n#{error}" + end + end + end + end + end + + [total_files, copied_files] + end + + private + + def process_file(file_path) + if text_file?(file_path) + content = File.read(file_path, encoding: "utf-8") + [file_path, content, nil] + elsif @include_binary + content = encode_binary_file(file_path) + [file_path, content, nil] + else + [file_path, nil, "Skipped binary file"] + end + rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError + @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 new file mode 100644 index 0000000..d33b2ca --- /dev/null +++ b/lib/poepod/gem_processor.rb @@ -0,0 +1,79 @@ +# lib/poepod/gem_processor.rb +require_relative "processor" +require "rubygems/specification" +require "git" + +module Poepod + class GemProcessor < Processor + def initialize(gemspec_path, config_file = nil, include_unstaged = false) + super(config_file) + @gemspec_path = gemspec_path + @include_unstaged = include_unstaged + end + + def process + unless File.exist?(@gemspec_path) + return [false, "Error: The specified gemspec file '#{@gemspec_path}' does not exist."] + end + + begin + spec = Gem::Specification.load(@gemspec_path) + rescue => e + return [false, "Error loading gemspec: #{e.message}"] + end + + gem_name = spec.name + output_file = "#{gem_name}_wrapped.txt" + unstaged_files = check_unstaged_files + + File.open(output_file, "w") do |file| + file.puts "# Wrapped Gem: #{gem_name}" + file.puts "## Gemspec: #{File.basename(@gemspec_path)}" + + if unstaged_files.any? + file.puts "\n## Warning: Unstaged Files" + file.puts unstaged_files.join("\n") + file.puts "\nThese files are not included in the wrap unless --include-unstaged option is used." + end + + 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.uniq.each do |relative_path| + full_path = File.join(File.dirname(@gemspec_path), relative_path) + if 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 + end + end + + [true, output_file, unstaged_files] + end + + private + + 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 } + end + + def check_unstaged_files + gem_root = File.dirname(@gemspec_path) + git = Git.open(gem_root) + + untracked_files = git.status.untracked.keys + modified_files = git.status.changed.keys + + (untracked_files + modified_files).select do |file| + file.start_with?("lib/", "spec/", "test/") + end + rescue Git::GitExecuteError => e + warn "Git error: #{e.message}. Assuming no unstaged files." + [] + end + end +end diff --git a/lib/poepod/processor.rb b/lib/poepod/processor.rb index 9ba78e6..599bc3f 100644 --- a/lib/poepod/processor.rb +++ b/lib/poepod/processor.rb @@ -1,20 +1,7 @@ -# frozen_string_literal: true - -require "yaml" -require "tqdm" -require "pathname" - +# lib/poepod/processor.rb module Poepod class Processor - EXCLUDE_DEFAULT = [ - /node_modules\//, /.git\//, /.gitignore$/, /.DS_Store$/, - /.jpg$/, /.jpeg$/, /.png/, /.svg$/, /.gif$/, - /.exe$/, /.dll$/, /.so$/, /.bin$/, /.o$/, /.a$/, /.gem$/, /.cap$/, - /.zip$/, - ].freeze - def initialize(config_file = nil) - @failed_files = [] @config = load_config(config_file) end @@ -26,61 +13,8 @@ def load_config(config_file) end end - def process_file(file_path) - content = File.read(file_path, encoding: "utf-8") - [file_path, content, nil] - rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError - @failed_files << file_path - [file_path, nil, "Failed to decode the file, as it is not saved with UTF-8 encoding."] - end - - def gather_files(directory_path, exclude) - exclude += @config["exclude"] if @config["exclude"] - exclude_pattern = Regexp.union(exclude.map { |ex| Regexp.new(ex) }) - - Dir.glob("#{directory_path}/**/*").reject do |file_path| - File.directory?(file_path) || file_path.match?(exclude_pattern) - end.map do |file_path| - Pathname.new(file_path).expand_path.to_s - end - end - - def write_results_to_file(results, output_file) - results.each_with_index do |(file_path, content, error), index| - relative = relative_path(file_path) - if content - output_file.puts "--- START FILE: #{relative} ---" - output_file.puts content - output_file.puts "--- END FILE: #{relative} ---" - elsif error - output_file.puts "#{relative}\n#{error}" - end - output_file.puts if index < results.size - 1 # Add a newline between files - end - end - - def relative_path(file_path) - Pathname.new(file_path).relative_path_from(Dir.pwd) - end - - def write_directory_structure_to_file(directory_path, output_file_name, exclude = EXCLUDE_DEFAULT) - dir_path = Pathname.new(directory_path) - - dir_path = dir_path.expand_path unless dir_path.absolute? - - file_list = gather_files(dir_path, exclude) - total_files = file_list.size - - File.open(output_file_name, "w", encoding: "utf-8") do |output_file| - results = file_list.tqdm(desc: "Progress", unit: " file").map do |file| - process_file(file) - end - write_results_to_file(results, output_file) - end - - copied_files = total_files - @failed_files.size - - [total_files, copied_files] + def process + raise NotImplementedError, "Subclasses must implement the 'process' method" end end end diff --git a/poepod.gemspec b/poepod.gemspec index 0643cfc..9f65d8f 100644 --- a/poepod.gemspec +++ b/poepod.gemspec @@ -16,10 +16,6 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/riboseinc/poepod" spec.license = "BSD-2-Clause" - spec.bindir = "bin" - spec.require_paths = ["lib"] - spec.files = `git ls-files`.split("\n") - spec.test_files = `git ls-files -- {spec}/*`.split("\n") spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0") # Specify which files should be added to the gem when it is released. @@ -32,10 +28,14 @@ Gem::Specification.new do |spec| spec.bindir = "exe" spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] + spec.test_files = `git ls-files -- spec/*`.split("\n") spec.add_runtime_dependency "parallel", "~> 1.20" spec.add_runtime_dependency "thor", "~> 1.0" spec.add_runtime_dependency "tqdm" + spec.add_runtime_dependency "mime-types", "~> 3.3" + spec.add_runtime_dependency "git", "~> 1.11" + spec.add_development_dependency "rake" spec.add_development_dependency "rspec" spec.add_development_dependency "rubocop" diff --git a/spec/poepod/cli_spec.rb b/spec/poepod/cli_spec.rb new file mode 100644 index 0000000..8abd531 --- /dev/null +++ b/spec/poepod/cli_spec.rb @@ -0,0 +1,77 @@ +require "spec_helper" +require "poepod/cli" + +RSpec.describe Poepod::Cli do + let(:cli) { described_class.new } + + describe "#concat" do + let(:temp_dir) { Dir.mktmpdir } + let(:text_file) { File.join(temp_dir, "text_file.txt") } + let(:binary_file) { File.join(temp_dir, "binary_file.bin") } + + before do + File.write(text_file, "Hello, World!") + File.write(binary_file, [0xFF, 0xD8, 0xFF, 0xE0].pack("C*")) + end + + after do + FileUtils.remove_entry(temp_dir) + end + + it "concatenates text files" do + output_file = File.join(temp_dir, "output.txt") + expect { cli.concat(text_file, output_file: output_file) }.to output(/1 files detected/).to_stdout + expect(File.exist?(output_file)).to be true + expect(File.read(output_file)).to include("Hello, World!") + end + + it "excludes binary files by default" do + output_file = File.join(temp_dir, "output.txt") + expect { cli.concat(text_file, binary_file, output_file: output_file) }.to output(/-> 2 files detected\.\n=> 1 files have been concatenated into.*\.txt/).to_stdout + end + + it "includes binary files when specified" do + output_file = File.join(temp_dir, "output.txt") + expect { cli.invoke(:concat, [text_file, binary_file], output_file: output_file, include_binary: true) }.to output(/-> 2 files detected\.\n=> 2 files have been concatenated into.*\.txt/).to_stdout + end + end + + describe "#wrap" do + let(:temp_dir) { Dir.mktmpdir } + let(:gemspec_file) { File.join(temp_dir, "test_gem.gemspec") } + + before do + File.write(gemspec_file, <<~GEMSPEC) + Gem::Specification.new do |spec| + spec.name = "test_gem" + spec.version = "0.1.0" + spec.authors = ["Test Author"] + spec.files = ["lib/test_gem.rb"] + end + GEMSPEC + + FileUtils.mkdir_p(File.join(temp_dir, "lib")) + File.write(File.join(temp_dir, "lib/test_gem.rb"), "puts 'Hello from test_gem'") + end + + after do + FileUtils.remove_entry(temp_dir) + end + + it "wraps a gem" do + expect { cli.wrap(gemspec_file) }.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("# 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 ---") + end + + it "handles non-existent gemspec" do + expect { cli.wrap("non_existent.gemspec") }.to output(/Error: The specified gemspec file/).to_stdout.and raise_error(SystemExit) + end + end +end diff --git a/spec/poepod/file_processor_spec.rb b/spec/poepod/file_processor_spec.rb new file mode 100644 index 0000000..19f8fc4 --- /dev/null +++ b/spec/poepod/file_processor_spec.rb @@ -0,0 +1,61 @@ +# spec/poepod/file_processor_spec.rb +require "spec_helper" +require "poepod/file_processor" +require "tempfile" + +RSpec.describe Poepod::FileProcessor do + let(:temp_dir) { Dir.mktmpdir } + let(:output_file) { Tempfile.new("output.txt") } + let(:text_file1) { File.join(temp_dir, "file1.txt") } + let(:text_file2) { File.join(temp_dir, "file2.txt") } + let(:binary_file) { File.join(temp_dir, "binary_file.bin") } + + before do + File.write(text_file1, "Content of file1.\n") + File.write(text_file2, "Content of file2.\n") + File.write(binary_file, [0xFF, 0xD8, 0xFF, 0xE0].pack("C*")) + end + + after do + FileUtils.remove_entry(temp_dir) + output_file.unlink + end + + let(:processor) { described_class.new([text_file1, text_file2], output_file.path) } + + describe "#process" do + it "processes text files and writes them to the output file" do + total_files, copied_files = processor.process + 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 + --- START FILE: #{text_file1} --- + Content of file1. + --- END FILE: #{text_file1} --- + --- START FILE: #{text_file2} --- + Content of file2. + --- END FILE: #{text_file2} --- + TEXT + expect(output_content).to eq(expected_content) + end + end + + describe "#process_file" do + 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 + 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.") + end + end +end diff --git a/spec/poepod/gem_processor_spec.rb b/spec/poepod/gem_processor_spec.rb new file mode 100644 index 0000000..4f21eb9 --- /dev/null +++ b/spec/poepod/gem_processor_spec.rb @@ -0,0 +1,107 @@ +# spec/poepod/gem_processor_spec.rb +require "spec_helper" +require "poepod/gem_processor" +require "tempfile" + +RSpec.describe Poepod::GemProcessor do + let(:temp_dir) { Dir.mktmpdir } + let(:gemspec_file) { File.join(temp_dir, "test_gem.gemspec") } + + before do + File.write(gemspec_file, <<~GEMSPEC) + Gem::Specification.new do |spec| + spec.name = "test_gem" + spec.version = "0.1.0" + spec.authors = ["Test Author"] + spec.files = ["lib/test_gem.rb"] + spec.test_files = ["spec/test_gem_spec.rb"] + end + GEMSPEC + + FileUtils.mkdir_p(File.join(temp_dir, "lib")) + FileUtils.mkdir_p(File.join(temp_dir, "spec")) + File.write(File.join(temp_dir, "lib/test_gem.rb"), "puts 'Hello from test_gem'") + File.write(File.join(temp_dir, "spec/test_gem_spec.rb"), "RSpec.describe TestGem do\nend") + File.write(File.join(temp_dir, "README.md"), "# Test Gem\n\nThis is a test gem.") + File.write(File.join(temp_dir, "README.txt"), "Test Gem\n\nThis is a test gem in plain text.") + end + + after do + FileUtils.remove_entry(temp_dir) + end + + describe "#process" do + let(:processor) { described_class.new(gemspec_file) } + + it "processes the gem files, includes README files, and spec files" do + success, output_file = processor.process + expect(success).to be true + 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 ---") + 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 ---") + end + + context "with non-existent gemspec" do + let(:processor) { described_class.new("non_existent.gemspec") } + + it "returns an error" do + success, error_message = processor.process + expect(success).to be false + expect(error_message).to include("Error: The specified gemspec file") + end + end + + context "with unstaged files" do + 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(: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, true) } + + it "includes unstaged files" do + allow(File).to receive(:file?).and_return(true) + allow(File).to receive(:read).and_return("Unstaged content") + + 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("--- START FILE: lib/unstaged_file.rb ---") + expect(content).to include("Unstaged content") + end + end + end + end +end diff --git a/spec/poepod/processor_spec.rb b/spec/poepod/processor_spec.rb deleted file mode 100644 index 8662115..0000000 --- a/spec/poepod/processor_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require "rspec" -require_relative "../../lib/poepod/processor" -require "tempfile" - -RSpec.describe Poepod::Processor do - let(:directory_path) { File.expand_path("#{__dir__}/../support/test_files") } - let(:output_file_name) { Tempfile.new("output.txt") } - let(:processor) { described_class.new } - - before do - File.write("#{directory_path}/file1.txt", "Content of file1.\n") - File.write("#{directory_path}/file2.txt", "Content of file2.\n") - end - - after do - File.delete(output_file_name) if File.exist?(output_file_name) - end - - describe "#process_file" do - it "reads the content of a file" do - file_path = "#{directory_path}/file1.txt" - _, content, error = processor.process_file(file_path) - expect(content).to eq("Content of file1.\n") - expect(error).to be_nil - end - - it "handles encoding errors gracefully" do - allow(File).to receive(:read).and_raise(Encoding::InvalidByteSequenceError) - file_path = "#{directory_path}/file1.txt" - _, content, error = processor.process_file(file_path) - expect(content).to be_nil - expect(error).to eq("Failed to decode the file, as it is not saved with UTF-8 encoding.") - end - end - - describe "#gather_files" do - it "gathers all file paths in a directory" do - files = processor.gather_files(directory_path, []) - expect(files).to contain_exactly( - "#{directory_path}/file1.txt", - "#{directory_path}/file2.txt" - ) - end - end - - describe "#write_results_to_file" do - it "writes the processed files to the output file" do - results = [ - ["#{directory_path}/file1.txt", "Content of file1.\n", nil], - ["#{directory_path}/file2.txt", "Content of file2.\n", nil] - ] - File.open(output_file_name, "w", encoding: "utf-8") do |output_file| - processor.write_results_to_file(results, output_file) - end - output_content = File.read(output_file_name, encoding: "utf-8") - expected_content = <<~TEXT - --- START FILE: spec/support/test_files/file1.txt --- - Content of file1. - --- END FILE: spec/support/test_files/file1.txt --- - - --- START FILE: spec/support/test_files/file2.txt --- - Content of file2. - --- END FILE: spec/support/test_files/file2.txt --- - TEXT - expect(output_content).to eq(expected_content) - end - end - - describe "#write_directory_structure_to_file" do - it "writes the directory structure to the output file with a progress bar" do - puts "directory_path: #{directory_path}" - puts Dir.glob("#{directory_path}/*") - total_files, copied_files = processor.write_directory_structure_to_file(directory_path, output_file_name) - expect(total_files).to eq(2) - expect(copied_files).to eq(2) - - output_content = File.read(output_file_name, encoding: "utf-8") - expected_content = <<~TEXT - --- START FILE: spec/support/test_files/file1.txt --- - Content of file1. - --- END FILE: spec/support/test_files/file1.txt --- - - --- START FILE: spec/support/test_files/file2.txt --- - Content of file2. - --- END FILE: spec/support/test_files/file2.txt --- - TEXT - expect(output_content).to eq(expected_content) - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dfb6a5f..d3ff6c5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "poepod" +require "fileutils" RSpec.configure do |config| # Enable flags like --only-failures and --next-failure