Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add wrap command to wrap gems #3

Merged
merged 3 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions .github/workflows/rake.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
# Auto-generated by Cimas: Do not edit it manually!
# See https://github.com/metanorma/cimas
name: rake

on:
push:
branches: [ master, main ]
tags: [ v* ]
pull_request:
workflow_dispatch:

jobs:
rake:
uses: metanorma/ci/.github/workflows/generic-rake.yml@main
secrets:
pat_token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
uses: metanorma/ci/.github/workflows/graphviz-rake.yml@main
11 changes: 5 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
# Auto-generated by Cimas: Do not edit it manually!
# See https://github.com/metanorma/cimas
name: release

on:
Expand All @@ -10,14 +8,15 @@ on:
Next release version. Possible values: x.y.z, major, minor, patch or pre|rc|etc
required: true
default: 'skip'
repository_dispatch:
types: [ do-release ]
push:
tags: [ v* ]

jobs:
release:
uses: metanorma/ci/.github/workflows/rubygems-release.yml@main
with:
next_version: ${{ github.event.inputs.next_version }}
secrets:
rubygems-api-key: ${{ secrets.METANORMA_CI_RUBYGEMS_API_KEY }}
pat_token: ${{ secrets.METANORMA_CI_PAT_TOKEN }}
rubygems-api-key: ${{ secrets.LUTAML_CI_RUBYGEMS_API_KEY }}
pat_token: ${{ secrets.LUTAML_CI_PAT_TOKEN }}

101 changes: 79 additions & 22 deletions README.adoc
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -81,4 +137,5 @@ https://rubygems.org.

== Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/riboseinc/poepod.
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].
68 changes: 52 additions & 16 deletions lib/poepod/cli.rb
Original file line number Diff line number Diff line change
@@ -1,36 +1,72 @@
# 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

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])
processor = Poepod::FileProcessor.new(files, output_path, options[:config], options[:include_binary])
total_files, copied_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)}."
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"

def wrap(gemspec_path)
processor = Poepod::GemProcessor.new(gemspec_path, nil, options[:include_unstaged])
success, result, unstaged_files = processor.process

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
79 changes: 79 additions & 0 deletions lib/poepod/file_processor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# frozen_string_literal: true

require_relative "processor"
require "yaml"
require "tqdm"
require "pathname"
require "open3"
require "base64"
require "mime/types"

module Poepod
class FileProcessor < Processor
EXCLUDE_DEFAULT = [
%r{node_modules/}, %r{.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|
next unless 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

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