From dfc006f3cfc66beb207ecbe52b6764bf333cc743 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Fri, 13 Jun 2025 13:05:24 -0300 Subject: [PATCH 01/14] Add docs and a CHANGELOG entry for `--only` option --- CHANGELOG.md | 2 ++ README.md | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 175fdac..68cbcc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [Unreleased] +- Add `--only` option to only include specific files or directories in the diff. + ## [0.5.0] - 2025-03-10 - Don't abort process on bundle check failure. diff --git a/README.md b/README.md index 5e0ac88..0926e51 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,14 @@ Skip specific files or directories during the diff. rails-diff generated scaffold Post --skip app/views app/helpers ``` +#### --only + +Only include specific files or directories in the diff. + +```bash +rails-diff generated scaffold Post --only app/models app/controllers +``` + #### --clear-cache/--no-cache Clear the cache directory to force cloning Rails and regenerating the Rails template app. From ac4064fbc7c6850c1f8bffd8de624e5fed657a6d Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Fri, 13 Jun 2025 13:05:48 -0300 Subject: [PATCH 02/14] Add standard --- Gemfile | 1 + Gemfile.lock | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Gemfile b/Gemfile index aa82799..cd3a8c3 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ source "https://rubygems.org/" # Specify your gem's dependencies in rails-diff.gemspec gemspec +gem "standard", "~> 1.0" gem "rake", "~> 13.0" gem "rspec", "~> 3.0" diff --git a/Gemfile.lock b/Gemfile.lock index 6cc3972..226e3d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -80,6 +80,7 @@ GEM securerandom (>= 0.3) tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) + ast (2.4.3) base64 (0.2.0) benchmark (0.4.0) bigdecimal (3.1.9) @@ -101,6 +102,9 @@ GEM pp (>= 0.6.0) rdoc (>= 4.0.0) reline (>= 0.4.2) + json (2.12.2) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) logger (1.6.6) loofah (2.24.0) crass (~> 1.0.2) @@ -129,9 +133,14 @@ GEM racc (~> 1.4) nokogiri (1.18.8-x86_64-darwin) racc (~> 1.4) + parallel (1.27.0) + parser (3.3.8.0) + ast (~> 2.4.1) + racc pp (0.6.2) prettyprint prettyprint (0.2.0) + prism (1.4.0) psych (5.2.3) date stringio @@ -173,9 +182,11 @@ GEM rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) + rainbow (3.1.1) rake (13.2.1) rdoc (6.12.0) psych (>= 4.0.0) + regexp_parser (2.10.0) reline (0.6.0) io-console (~> 0.5) rspec (3.13.0) @@ -191,12 +202,46 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.2) + rubocop (1.75.8) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (~> 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.44.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.45.1) + parser (>= 3.3.7.2) + prism (~> 1.4) + rubocop-performance (1.25.0) + lint_roller (~> 1.1) + rubocop (>= 1.75.0, < 2.0) + rubocop-ast (>= 1.38.0, < 2.0) + ruby-progressbar (1.13.0) securerandom (0.4.1) + standard (1.50.0) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.75.5) + standard-custom (~> 1.0.0) + standard-performance (~> 1.8) + standard-custom (1.0.2) + lint_roller (~> 1.0) + rubocop (~> 1.50) + standard-performance (1.8.0) + lint_roller (~> 1.1) + rubocop-performance (~> 1.25.0) stringio (3.1.4) thor (1.3.2) timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unicode-display_width (3.1.4) + unicode-emoji (~> 4.0, >= 4.0.4) + unicode-emoji (4.0.4) uri (1.0.3) useragent (0.16.11) websocket-driver (0.7.7) @@ -213,6 +258,7 @@ DEPENDENCIES rails-diff! rake (~> 13.0) rspec (~> 3.0) + standard (~> 1.0) BUNDLED WITH 2.6.2 From 2abd165e3200fbd8d7a7bf4d4d5ebbff5ffcb02e Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Fri, 13 Jun 2025 13:07:00 -0300 Subject: [PATCH 03/14] Internal refactoring --- lib/rails/diff.rb | 234 ++---------------- lib/rails/diff/cli.rb | 56 +++++ lib/rails/diff/file_tracker.rb | 46 ++-- lib/rails/diff/logger.rb | 21 ++ lib/rails/diff/rails_app_generator.rb | 95 +++++++ lib/rails/diff/rails_repo.rb | 88 +++++++ .../rails/diff/file_tracker_spec.rb | 110 ++++---- spec/lib/rails/diff/rails_repo_spec.rb | 145 +++++++++++ spec/rails/diff_spec.rb | 36 --- spec/spec_helper.rb | 2 +- spec/support/git_repo.rb | 52 ++++ 11 files changed, 564 insertions(+), 321 deletions(-) create mode 100644 lib/rails/diff/cli.rb create mode 100644 lib/rails/diff/logger.rb create mode 100644 lib/rails/diff/rails_app_generator.rb create mode 100644 lib/rails/diff/rails_repo.rb create mode 100644 spec/lib/rails/diff/rails_repo_spec.rb delete mode 100644 spec/rails/diff_spec.rb create mode 100644 spec/support/git_repo.rb diff --git a/lib/rails/diff.rb b/lib/rails/diff.rb index 03d33f9..14fbc25 100644 --- a/lib/rails/diff.rb +++ b/lib/rails/diff.rb @@ -1,130 +1,64 @@ # frozen_string_literal: true -require_relative "diff/version" -require_relative "diff/file_tracker" require "rails" -require "thor" require "diffy" require "fileutils" require "open3" +require_relative "diff/cli" +require_relative "diff/file_tracker" +require_relative "diff/logger" +require_relative "diff/rails_app_generator" +require_relative "diff/rails_repo" +require_relative "diff/version" module Rails module Diff - class Error < StandardError; end - - RAILS_REPO = "https://github.com/rails/rails.git" CACHE_DIR = File.expand_path("#{ENV["HOME"]}/.rails-diff/cache") - RAILSRC_PATH = "#{ENV["HOME"]}/.railsrc" class << self def file(*files, no_cache: false, commit: nil, new_app_options: nil) - clear_cache if no_cache - ensure_template_app_exists(commit, new_app_options) + app_generator = RailsAppGenerator.new(commit:, new_app_options:, no_cache:) + app_generator.create_template_app - files.filter_map { |it| diff_with_header(it) }.join("\n") + files + .filter_map { |it| diff_with_header(it, app_generator.template_app_path) } + .join("\n") end def generated(generator_name, *args, no_cache: false, skip: [], only: [], commit: nil, new_app_options: nil) - clear_cache if no_cache - ensure_template_app_exists(commit, new_app_options) - install_app_dependencies + app_generator = RailsAppGenerator.new(commit:, new_app_options:, no_cache:) + app_generator.create_template_app + app_generator.install_app_dependencies - generated_files(generator_name, *args, skip, only) - .map { |it| diff_with_header(it) } + app_generator.run_generator(generator_name, *args, skip, only) + .map { |it| diff_with_header(it, app_generator.template_app_path) } .join("\n\n") end - private - - def system!(*cmd, abort: true) + def system!(*cmd, logger:, abort: true) _, stderr, status = Open3.capture3(*cmd) - - debug cmd.join(" ") - + logger.debug(cmd.join(" ")) if status.success? true elsif abort - $stderr.puts "\e[1;31mCommand failed:\e[0m #{cmd.join(' ')}" + logger.error("Command failed:", cmd.join(" ")) abort stderr else false end end - def info(message) - puts "\e[1;34minfo:\e[0m\t#{message}" - end - - def debug(message) - return unless ENV["DEBUG"] - - puts "\e[1;33mdebug:\e[0m\t#{message}" - end - - def clear_cache - info "Clearing cache" - FileUtils.rm_rf(CACHE_DIR) - end - - def ensure_template_app_exists(commit, new_app_options) - FileUtils.mkdir_p(CACHE_DIR) - @new_app_options = new_app_options - @commit = commit || latest_commit - return if cached_app? - - create_new_rails_app - end - - def template_app_path - @template_app_path ||= File.join(CACHE_DIR, "rails-#{commit.first(10)}", rails_new_options_hash, app_name) - end - - def rails_path - @rails_path ||= begin - File.join(CACHE_DIR, "rails").tap do |path| - unless File.exist?(path) - info "Cloning Rails repository" - system!("git", "clone", "--depth", "1", RAILS_REPO, path) - end - end - end - end - - def railsrc_options - return @railsrc_options if defined?(@railsrc_options) - - @railsrc_options = File.read(RAILSRC_PATH).lines if File.exist?(RAILSRC_PATH) - end - - def app_name = @app_name ||= File.basename(Dir.pwd) - - def generated_files(generator_name, *args, skip, only) - Dir.chdir(template_app_path) do - system!("bin/rails", "destroy", generator_name, *args) - info "Running generator: rails generate #{generator_name} #{args.join(' ')}" - FileTracker.new.track_new_files(template_app_path, skip, only) { system!("bin/rails", "generate", generator_name, *args) } - .map { |it| it.delete_prefix("#{template_app_path}/") } - end - end + private - def diff_with_header(file) - diff = diff_file(file) + def diff_with_header(file, template_app_path) + diff = diff_file(file, template_app_path) return if diff.empty? header = "#{file} diff:" [header, "=" * header.size, diff].join("\n") end - def install_app_dependencies - Dir.chdir(template_app_path) do - unless system!("bundle check", abort: false) - info "Installing application dependencies" - system!("bundle install") - end - end - end - - def diff_file(file) + def diff_file(file, template_app_path) rails_file = File.join(template_app_path, file) repo_file = File.join(Dir.pwd, file) @@ -135,129 +69,9 @@ def diff_file(file) rails_file, repo_file, context: 2, - source: 'files' + source: "files" ).to_s(:color).chomp end - - def cached_app? - File.exist?(template_app_path) && !out_of_date_rails? - end - - def out_of_date_rails? - return true unless File.exist?(rails_path) - - Dir.chdir(rails_path) do - system!("git fetch origin main") - current = `git rev-parse HEAD`.strip - latest = `git rev-parse origin/main`.strip - - if current != latest - FileUtils.rm_rf(rails_path) - return true - end - end - - false - end - - def create_new_rails_app - Dir.chdir(rails_path) do - checkout_rails - generate_app - end - end - - def generate_app - Dir.chdir("railties") do - unless system!("bundle check", abort: false) - info "Installing Rails dependencies" - system!("bundle install") - end - - if railsrc_options - info "Using default options from #{RAILSRC_PATH}:\n\t > #{railsrc_options.join(' ')}" - end - - info "Generating new Rails application\n\t > #{rails_new_command.join(' ')}" - system!(*rails_new_command) - end - end - - def checkout_rails - info "Checking out Rails (at commit #{commit[0..6]})" - system!("git", "checkout", commit) - end - - def commit = @commit - - def new_app_options = @new_app_options - - def latest_commit - Dir.chdir(rails_path) do - `git rev-parse origin/main`.strip - end - end - - def rails_new_command = @rails_new_command ||= [ - "bundle", - "exec", - "rails", - "new", - template_app_path, - "--main", - "--skip-bundle", - "--force", - "--quiet", - *rails_new_options - ] - - def rails_new_options = @rails_new_options ||= [*new_app_options, *railsrc_options].compact - - def rails_new_options_hash = Digest::MD5.hexdigest(rails_new_options.join(" ")) - end - - class CLI < Thor - class_option :no_cache, type: :boolean, desc: "Clear cache before running", aliases: ["--clear-cache"] - class_option :fail_on_diff, type: :boolean, desc: "Fail if there are differences" - class_option :commit, type: :string, desc: "Compare against a specific commit" - class_option :new_app_options, type: :string, desc: "Options to pass to the rails new command" - class_option :debug, type: :boolean, desc: "Print debug information", aliases: ["-d"] - - def self.exit_on_failure? = true - - desc "file FILE [FILE ...]", "Compare one or more files from your repository with Rails' generated version" - def file(*files) - abort "Please provide at least one file to compare" if files.empty? - ENV["DEBUG"] = "true" if options[:debug] - - diff = Rails::Diff.file(*files, no_cache: options[:no_cache], commit: options[:commit], new_app_options: options[:new_app_options]) - return if diff.empty? - - options[:fail] ? abort(diff) : puts(diff) - end - - desc "generated GENERATOR [args]", "Compare files that would be created by a Rails generator" - option :skip, type: :array, desc: "Skip specific files or directories", aliases: ["-s"], default: [] - option :only, type: :array, desc: "Only include specific files or directories", aliases: ["-o"], default: [] - def generated(generator_name, *args) - ENV["DEBUG"] = "true" if options[:debug] - diff = Rails::Diff.generated(generator_name, - *args, - no_cache: options[:no_cache], - skip: options[:skip], - only: options[:only], - commit: options[:commit], - new_app_options: options[:new_app_options]) - return if diff.empty? - - options[:fail] ? abort(diff) : puts(diff) - end - - map %w[--version -v] => :__version - desc "--version, -v", "print the version" - def __version - puts VERSION - end end end end diff --git a/lib/rails/diff/cli.rb b/lib/rails/diff/cli.rb new file mode 100644 index 0000000..74a50bc --- /dev/null +++ b/lib/rails/diff/cli.rb @@ -0,0 +1,56 @@ +require "thor" + +module Rails + module Diff + class CLI < Thor + class_option :no_cache, type: :boolean, desc: "Clear cache before running", aliases: ["--clear-cache"] + class_option :fail_on_diff, type: :boolean, desc: "Fail if there are differences" + class_option :commit, type: :string, desc: "Compare against a specific commit" + class_option :new_app_options, type: :string, desc: "Options to pass to the rails new command" + class_option :debug, type: :boolean, desc: "Print debug information", aliases: ["-d"] + + def self.exit_on_failure? = true + + desc "file FILE [FILE ...]", "Compare one or more files from your repository with Rails' generated version" + def file(*files) + abort "Please provide at least one file to compare" if files.empty? + ENV["DEBUG"] = "true" if options[:debug] + + diff = Rails::Diff.file( + *files, + no_cache: options[:no_cache], + commit: options[:commit], + new_app_options: options[:new_app_options] + ) + return if diff.empty? + + options[:fail] ? abort(diff) : puts(diff) + end + + desc "generated GENERATOR [args]", "Compare files that would be created by a Rails generator" + option :skip, type: :array, desc: "Skip specific files or directories", aliases: ["-s"], default: [] + option :only, type: :array, desc: "Only include specific files or directories", aliases: ["-o"], default: [] + def generated(generator_name, *args) + ENV["DEBUG"] = "true" if options[:debug] + diff = Rails::Diff.generated( + generator_name, + *args, + no_cache: options[:no_cache], + skip: options[:skip], + only: options[:only], + commit: options[:commit], + new_app_options: options[:new_app_options] + ) + return if diff.empty? + + options[:fail] ? abort(diff) : puts(diff) + end + + map %w[--version -v] => :__version + desc "--version, -v", "print the version" + def __version + puts VERSION + end + end + end +end diff --git a/lib/rails/diff/file_tracker.rb b/lib/rails/diff/file_tracker.rb index 8468164..cd822f8 100644 --- a/lib/rails/diff/file_tracker.rb +++ b/lib/rails/diff/file_tracker.rb @@ -1,29 +1,39 @@ # frozen_string_literal: true -class FileTracker - def track_new_files(template_app_path, skip, only = []) - files_before = list_files(template_app_path) +module Rails + module Diff + class FileTracker + def initialize(base_dir, skip = [], only = []) + @base_dir = base_dir + @skip = skip + @only = only + end + + def new_files + files_before = list_files(@base_dir) yield - files_after = list_files(template_app_path, skip, only) + files_after = list_files(@base_dir, @skip, @only) files_after - files_before end private - def list_files(dir, skip = [], only = []) - files = Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).reject do |it| - File.directory?(it) || - it.start_with?("#{dir}/.git") || - it.start_with?("#{dir}/tmp") || - it.start_with?("#{dir}/log") || - it.start_with?("#{dir}/test") || - skip.any? { |s| it.start_with?("#{dir}/#{s}") } - end + def list_files(dir, skip = [], only = []) + files = Dir.glob("#{dir}/**/*", File::FNM_DOTMATCH).reject do |it| + File.directory?(it) || + it.start_with?("#{dir}/.git") || + it.start_with?("#{dir}/tmp") || + it.start_with?("#{dir}/log") || + it.start_with?("#{dir}/test") || + skip.any? { |s| it.start_with?("#{dir}/#{s}") } + end - if only.any? - files.select { |it| only.any? { |o| it.start_with?("#{dir}/#{o}") } } - else - files + if only.any? + files.select { |it| only.any? { |o| it.start_with?("#{dir}/#{o}") } } + else + files + end end end - end \ No newline at end of file + end +end diff --git a/lib/rails/diff/logger.rb b/lib/rails/diff/logger.rb new file mode 100644 index 0000000..f2e3f54 --- /dev/null +++ b/lib/rails/diff/logger.rb @@ -0,0 +1,21 @@ +module Rails + module Diff + module Logger + extend self + + def info(message) + puts "\e[1;34minfo:\e[0m\t#{message}" + end + + def debug(message) + return unless ENV["DEBUG"] + + puts "\e[1;33mdebug:\e[0m\t#{message}" + end + + def error(label, message) + warn "\e[1;31m#{label}\e[0m #{message}" + end + end + end +end diff --git a/lib/rails/diff/rails_app_generator.rb b/lib/rails/diff/rails_app_generator.rb new file mode 100644 index 0000000..a2ad3cf --- /dev/null +++ b/lib/rails/diff/rails_app_generator.rb @@ -0,0 +1,95 @@ +require "digest" + +module Rails + module Diff + class RailsAppGenerator + RAILSRC_PATH = "#{ENV["HOME"]}/.railsrc" + + def initialize(commit: nil, new_app_options: nil, no_cache: false, logger: Logger, cache_dir: Rails::Diff::CACHE_DIR) + @new_app_options = new_app_options.to_s.split + @rails_repo = RailsRepo.new(logger:, cache_dir:) + @commit = commit + @logger = logger + @cache_dir = cache_dir + clear_cache if no_cache + end + + def clear_cache + logger.info "Clearing cache" + FileUtils.rm_rf(cache_dir, secure: true) + FileUtils.mkdir_p(cache_dir) + end + + def create_template_app + return if cached_app? + + create_new_rails_app + end + + def template_app_path + @template_app_path ||= File.join(cache_dir, rails_cache_dir_key, rails_new_options_hash, app_name) + end + + def install_app_dependencies + Dir.chdir(template_app_path) do + unless Rails::Diff.system!("bundle check", abort: false, logger: logger) + logger.info "Installing application dependencies" + Rails::Diff.system!("bundle install", logger: logger) + end + end + end + + def run_generator(generator_name, *args, skip, only) + Dir.chdir(template_app_path) do + Rails::Diff.system!("bin/rails", "destroy", generator_name, *args, logger: logger) + logger.info "Running generator: rails generate #{generator_name} #{args.join(" ")}" + + FileTracker.new(template_app_path, skip, only) + .new_files { Rails::Diff.system!("bin/rails", "generate", generator_name, *args, logger: logger) } + .map { |it| it.delete_prefix("#{template_app_path}/") } + end + end + + private + + attr_reader :new_app_options, :rails_repo, :logger, :cache_dir + + def commit = @commit ||= rails_repo.latest_commit + + def rails_cache_dir_key = "rails-#{commit.first(10)}" + + def railsrc_options + @railsrc_options ||= File.exist?(RAILSRC_PATH) ? File.readlines(RAILSRC_PATH) : [] + end + + def app_name = @app_name ||= File.basename(Dir.pwd) + + def cached_app? + File.exist?(template_app_path) && rails_repo.up_to_date? + end + + def create_new_rails_app + checkout_rails_commit + generate_app + end + + def generate_app + rails_repo.install_dependencies + if railsrc_options.any? + logger.info "Using default options from #{RAILSRC_PATH}:\n\t > #{railsrc_options.join(" ")}" + end + rails_repo.new_app(template_app_path, rails_new_options) + end + + def checkout_rails_commit = rails_repo.checkout(commit) + + def rails_new_options + @rails_new_options ||= (new_app_options + railsrc_options).compact + end + + def rails_new_options_hash + Digest::MD5.hexdigest(rails_new_options.join(" ")) + end + end + end +end diff --git a/lib/rails/diff/rails_repo.rb b/lib/rails/diff/rails_repo.rb new file mode 100644 index 0000000..8b9a480 --- /dev/null +++ b/lib/rails/diff/rails_repo.rb @@ -0,0 +1,88 @@ +module Rails + module Diff + class RailsRepo + RAILS_REPO = "https://github.com/rails/rails.git" + + def initialize(logger:, cache_dir: Rails::Diff::CACHE_DIR, rails_repo: RAILS_REPO) + @logger = logger + @cache_dir = cache_dir + @rails_repo = rails_repo + end + + def checkout(commit) + on_rails_dir do + logger.info "Checking out Rails (at commit #{commit[0..6]})" + Rails::Diff.system!("git", "checkout", commit, logger: logger) + end + end + + def latest_commit + @latest_commit ||= on_rails_dir do + Rails::Diff.system!("git fetch origin main", logger: logger) + `git rev-parse origin/main`.strip + end + end + + def up_to_date? + File.exist?(rails_path) && on_latest_commit? + end + + def install_dependencies + within "railties" do + unless Rails::Diff.system!("bundle check", abort: false, logger: logger) + logger.info "Installing Rails dependencies" + Rails::Diff.system!("bundle install", logger: logger) + end + end + end + + def new_app(name, options) + within "railties" do + command = rails_new_command(name, options) + logger.info "Generating new Rails application\n\t > #{command.join(" ")}" + Rails::Diff.system!(*command, logger: logger) + end + end + + def within(dir, &block) = on_rails_dir { Dir.chdir(dir, &block) } + + private + + attr_reader :logger, :cache_dir, :rails_repo + + def rails_path + File.join(cache_dir, "rails") + end + + def on_latest_commit? + if current_commit == latest_commit + true + else + remove_repo + false + end + end + + def on_rails_dir(&block) + clone_repo unless File.exist?(rails_path) + Dir.chdir(rails_path, &block) + end + + def current_commit = on_rails_dir { `git rev-parse HEAD`.strip } + + def remove_repo = FileUtils.rm_rf(rails_path, secure: true) + + def clone_repo + logger.info "Cloning Rails repository" + Rails::Diff.system!("git", "clone", "--depth", "1", rails_repo, rails_path, logger: logger) + end + + def rails_new_command(name, options) + [ + "bundle", "exec", "rails", "new", name, + "--main", "--skip-bundle", "--force", "--quiet", *options + ] + end + end + end +end diff --git a/spec/integration/rails/diff/file_tracker_spec.rb b/spec/integration/rails/diff/file_tracker_spec.rb index e513e46..8e12db8 100644 --- a/spec/integration/rails/diff/file_tracker_spec.rb +++ b/spec/integration/rails/diff/file_tracker_spec.rb @@ -1,77 +1,75 @@ # frozen_string_literal: true -require 'rails/diff/file_tracker' +require "rails/diff/file_tracker" -RSpec.describe FileTracker do - context 'integration tests' do - let(:temp_dir) { Dir.mktmpdir } +RSpec.describe Rails::Diff::FileTracker do + let(:temp_dir) { Dir.mktmpdir } - after do - FileUtils.remove_entry(temp_dir) - end - - it 'tracks newly created files' do - FileUtils.touch("#{temp_dir}/file1.rb") - file_tracker = FileTracker.new + after do + FileUtils.remove_entry(temp_dir) + end - new_files = file_tracker.track_new_files(temp_dir, []) do - FileUtils.touch("#{temp_dir}/file2.rb") - FileUtils.touch("#{temp_dir}/file3.rb") - end + it "tracks newly created files" do + FileUtils.touch("#{temp_dir}/file1.rb") + file_tracker = described_class.new(temp_dir) - expect(new_files).to contain_exactly("#{temp_dir}/file2.rb", "#{temp_dir}/file3.rb") + new_files = file_tracker.new_files do + FileUtils.touch("#{temp_dir}/file2.rb") + FileUtils.touch("#{temp_dir}/file3.rb") end - it 'excludes skipped files' do - FileUtils.touch("#{temp_dir}/file1.rb") - file_tracker = FileTracker.new + expect(new_files).to contain_exactly("#{temp_dir}/file2.rb", "#{temp_dir}/file3.rb") + end - new_files = file_tracker.track_new_files(temp_dir, ['file2.rb']) do - FileUtils.touch("#{temp_dir}/file2.rb") - FileUtils.touch("#{temp_dir}/file3.rb") - end + it "excludes skipped files" do + FileUtils.touch("#{temp_dir}/file1.rb") + file_tracker = described_class.new(temp_dir, ["file2.rb"]) - expect(new_files).to contain_exactly("#{temp_dir}/file3.rb") + new_files = file_tracker.new_files do + FileUtils.touch("#{temp_dir}/file2.rb") + FileUtils.touch("#{temp_dir}/file3.rb") end - it 'handles files with --only option' do - FileUtils.touch("#{temp_dir}/file1.rb") - file_tracker = FileTracker.new - new_files = file_tracker.track_new_files(temp_dir, [], ['file2.rb']) do - FileUtils.touch("#{temp_dir}/file2.rb") - FileUtils.touch("#{temp_dir}/file3.rb") - end - expect(new_files).to contain_exactly("#{temp_dir}/file2.rb") + expect(new_files).to contain_exactly("#{temp_dir}/file3.rb") + end + + it "handles files with only option" do + FileUtils.touch("#{temp_dir}/file1.rb") + file_tracker = described_class.new(temp_dir, [], ["file2.rb"]) + new_files = file_tracker.new_files do + FileUtils.touch("#{temp_dir}/file2.rb") + FileUtils.touch("#{temp_dir}/file3.rb") end + expect(new_files).to contain_exactly("#{temp_dir}/file2.rb") + end - it 'ignores files in special directories' do - FileUtils.mkdir_p("#{temp_dir}/.git") - FileUtils.mkdir_p("#{temp_dir}/tmp") - FileUtils.mkdir_p("#{temp_dir}/log") - FileUtils.touch("#{temp_dir}/file1.rb") - file_tracker = FileTracker.new - - new_files = file_tracker.track_new_files(temp_dir, []) do - FileUtils.touch("#{temp_dir}/.git/config") - FileUtils.touch("#{temp_dir}/tmp/cache") - FileUtils.touch("#{temp_dir}/log/development.log") - FileUtils.touch("#{temp_dir}/file2.rb") - end - - expect(new_files).to contain_exactly("#{temp_dir}/file2.rb") + it "ignores files in special directories" do + FileUtils.mkdir_p("#{temp_dir}/.git") + FileUtils.mkdir_p("#{temp_dir}/tmp") + FileUtils.mkdir_p("#{temp_dir}/log") + FileUtils.touch("#{temp_dir}/file1.rb") + file_tracker = described_class.new(temp_dir) + + new_files = file_tracker.new_files do + FileUtils.touch("#{temp_dir}/.git/config") + FileUtils.touch("#{temp_dir}/tmp/cache") + FileUtils.touch("#{temp_dir}/log/development.log") + FileUtils.touch("#{temp_dir}/file2.rb") end - it 'handles nested directories' do - FileUtils.touch("#{temp_dir}/file1.rb") - file_tracker = FileTracker.new + expect(new_files).to contain_exactly("#{temp_dir}/file2.rb") + end - new_files = file_tracker.track_new_files(temp_dir, []) do - FileUtils.mkdir_p("#{temp_dir}/nested/dir") - FileUtils.touch("#{temp_dir}/nested/file2.rb") - FileUtils.touch("#{temp_dir}/nested/dir/file3.rb") - end + it "handles nested directories" do + FileUtils.touch("#{temp_dir}/file1.rb") + file_tracker = described_class.new(temp_dir) - expect(new_files).to contain_exactly("#{temp_dir}/nested/file2.rb", "#{temp_dir}/nested/dir/file3.rb") + new_files = file_tracker.new_files do + FileUtils.mkdir_p("#{temp_dir}/nested/dir") + FileUtils.touch("#{temp_dir}/nested/file2.rb") + FileUtils.touch("#{temp_dir}/nested/dir/file3.rb") end + + expect(new_files).to contain_exactly("#{temp_dir}/nested/file2.rb", "#{temp_dir}/nested/dir/file3.rb") end end diff --git a/spec/lib/rails/diff/rails_repo_spec.rb b/spec/lib/rails/diff/rails_repo_spec.rb new file mode 100644 index 0000000..1c50040 --- /dev/null +++ b/spec/lib/rails/diff/rails_repo_spec.rb @@ -0,0 +1,145 @@ +# Helper indices for commits: +# git_repo.commits[0] => 'add railties dir' (initial commit) +# git_repo.commits[1] => 'commit1' +# git_repo.commits.last => 'commit2' + +require "rails/diff/rails_repo" +require "fileutils" +require "tmpdir" + +RSpec.describe Rails::Diff::RailsRepo do + let(:logger) { spy } + let(:cache_dir) { Dir.mktmpdir } + let(:rails_path) { File.join(cache_dir, "rails") } + let(:git_repo) do + GitRepo.new.tap do |repo| + repo.add_commit("commit1") + repo.add_commit("commit2") + end + end + + after do + FileUtils.rm_rf(cache_dir) + git_repo.cleanup + end + + describe "#up_to_date?" do + it "returns false when repo is not cloned yet" do + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + result = repo.up_to_date? + + expect(result).to eq false + end + + it "returns true when repo is on the latest commit" do + git_repo.clone_at_commit(git_repo.commits.last, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + result = repo.up_to_date? + + expect(result).to eq true + end + + it "returns false and removes the repo when repo is on an old commit" do + git_repo.clone_at_commit(git_repo.commits.first, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + result = repo.up_to_date? + + expect(result).to eq false + expect(File.exist?(rails_path)).to eq false + end + end + + describe "#latest_commit" do + it "returns the latest commit SHA from the remote repo" do + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + latest = repo.latest_commit + + expect(latest).to eq git_repo.commits.last + end + + it "returns the new latest commit after a new commit is pushed" do + new_commit = git_repo.add_commit("commit3") + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + latest = repo.latest_commit + + expect(latest).to eq new_commit + end + end + + describe "#checkout" do + it "checks out the given commit in the repo" do + git_repo.clone_at_commit(git_repo.commits.last, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + # Checkout the first file commit (not the initial railties commit) + repo.checkout(git_repo.commits[1]) + + # Verify HEAD is now at the first file commit + current = Dir.chdir(rails_path) { `git rev-parse HEAD`.strip } + expect(current).to eq git_repo.commits[1] + end + + it "logs the checkout info message" do + git_repo.clone_at_commit(git_repo.commits.last, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + repo.checkout(git_repo.commits[1]) + + expect(logger).to have_received(:info) + .with(/Checking out Rails \(at commit #{git_repo.commits[1][0..6]}\)/) + end + end + + describe "#install_dependencies" do + it "runs bundle check and bundle install if needed, and logs appropriately" do + git_repo.clone_at_commit(git_repo.commits[0], rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + # Simulate bundle check failing, so bundle install is needed + allow(Rails::Diff).to receive(:system!).with("bundle check", abort: false, logger: logger).and_return(false) + allow(Rails::Diff).to receive(:system!).with("bundle install", logger: logger).and_return(true) + + repo.install_dependencies + + expect(logger).to have_received(:info).with("Installing Rails dependencies") + expect(Rails::Diff).to have_received(:system!).with("bundle install", logger: logger) + end + + it "does not run bundle install if bundle check passes" do + git_repo.clone_at_commit(git_repo.commits[0], rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + # Simulate bundle check passing + allow(Rails::Diff).to receive(:system!).with("bundle check", abort: false, logger: logger).and_return(true) + + repo.install_dependencies + + expect(logger).not_to have_received(:info).with("Installing Rails dependencies") + end + end + + describe "#new_app" do + it "runs the rails new command with the correct arguments and logs the command" do + git_repo.clone_at_commit(git_repo.commits.last, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + + allow(Rails::Diff).to receive(:system!).and_return(true) + app_name = "myapp" + options = ["--skip-test"] + + repo.new_app(app_name, options) + + expected_command = [ + "bundle", "exec", "rails", "new", app_name, + "--main", "--skip-bundle", "--force", "--quiet", *options + ] + expect(Rails::Diff).to have_received(:system!).with(*expected_command, logger: logger) + expect(logger).to have_received(:info).with(/Generating new Rails application/) + end + end +end diff --git a/spec/rails/diff_spec.rb b/spec/rails/diff_spec.rb deleted file mode 100644 index 24b4d43..0000000 --- a/spec/rails/diff_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -RSpec.describe Rails::Diff do - it "has a version number" do - expect(Rails::Diff::VERSION).not_to be nil - end - - describe ".generated" do - before do - allow(Rails::Diff).to receive(:system!).and_return(true) - allow(Rails::Diff).to receive(:ensure_template_app_exists) - allow(Rails::Diff).to receive(:install_app_dependencies) - allow(Rails::Diff).to receive(:generated_files).and_return(["file1.rb", "file2.rb"]) - allow(Rails::Diff).to receive(:diff_with_header).and_return("file1.rb diff:\n===\nDiff content") - end - - it "returns the diff for generated files" do - result = Rails::Diff.generated("model", "User", no_cache: true) - expect(result).to include("file1.rb diff:") - end - end - - describe Rails::Diff::CLI do - describe "#generated" do - before do - allow(Rails::Diff).to receive(:system!).and_return(true) - allow(Rails::Diff).to receive(:generated).and_return("file1.rb diff:\n===\nDiff content") - end - - it "runs without error" do - cli = Rails::Diff::CLI.new - expect { cli.generated("model", "User") }.not_to raise_error - end - end - end -end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 70fce95..79b1be2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require "rails/diff" -$LOAD_PATH.unshift(File.expand_path('../../lib', __dir__)) +Dir["./spec/support/**/*.rb"].sort.each { |f| require f } RSpec.configure do |config| # Enable flags like --only-failures and --next-failure diff --git a/spec/support/git_repo.rb b/spec/support/git_repo.rb new file mode 100644 index 0000000..1170c86 --- /dev/null +++ b/spec/support/git_repo.rb @@ -0,0 +1,52 @@ +require "fileutils" +require "tmpdir" + +class GitRepo + attr_reader :remote_repo, :commits + + # Initializes and creates a bare remote repo and a working repo with main branch + def initialize + @remote_dir = Dir.mktmpdir + @remote_repo = File.join(@remote_dir, "origin.git") + Dir.chdir(@remote_dir) { `git init --bare origin.git > /dev/null 2>&1` } + + @commits = [] + @work_dir = Dir.mktmpdir + Dir.chdir(@work_dir) do + `git clone #{@remote_repo} . > /dev/null 2>&1` + `git checkout -b main > /dev/null 2>&1` + # Always create the 'railties' directory for test convenience + FileUtils.mkdir_p("railties") + File.write("railties/.keep", "keep") + `git add railties > /dev/null 2>&1` + `git commit -m "add railties dir" > /dev/null 2>&1` + @commits << `git rev-parse HEAD`.strip + `git push origin main > /dev/null 2>&1` + end + end + + # Adds a commit with the given message and pushes to remote + def add_commit(message) + Dir.chdir(@work_dir) do + filename = "file#{@commits.size}.txt" + File.write(filename, message) + `git add . > /dev/null 2>&1` + `git commit -m "#{message}" > /dev/null 2>&1` + sha = `git rev-parse HEAD`.strip + @commits << sha + `git push origin main > /dev/null 2>&1` + end + + @commits.last + end + + def clone_at_commit(commit_sha, dest_path) + `git clone #{@remote_repo} #{dest_path} > /dev/null 2>&1` + Dir.chdir(dest_path) { `git checkout #{commit_sha} > /dev/null 2>&1` } + end + + def cleanup + FileUtils.rm_rf(@remote_dir) + FileUtils.rm_rf(@work_dir) + end +end From ed2b002fbb0e7390bce863c702573fa743b24dbe Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Fri, 13 Jun 2025 22:25:32 -0300 Subject: [PATCH 04/14] Add simplecov --- Gemfile | 1 + Gemfile.lock | 8 ++++++++ spec/spec_helper.rb | 6 ++++++ 3 files changed, 15 insertions(+) diff --git a/Gemfile b/Gemfile index cd3a8c3..e2ec273 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ source "https://rubygems.org/" # Specify your gem's dependencies in rails-diff.gemspec gemspec +gem "simplecov", require: false gem "standard", "~> 1.0" gem "rake", "~> 13.0" diff --git a/Gemfile.lock b/Gemfile.lock index 226e3d2..a0f0a7a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -91,6 +91,7 @@ GEM date (3.4.1) diff-lcs (1.5.1) diffy (3.4.3) + docile (1.4.1) drb (2.2.1) erubi (1.13.1) globalid (1.2.1) @@ -222,6 +223,12 @@ GEM rubocop-ast (>= 1.38.0, < 2.0) ruby-progressbar (1.13.0) securerandom (0.4.1) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.1) + simplecov_json_formatter (0.1.4) standard (1.50.0) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.0) @@ -258,6 +265,7 @@ DEPENDENCIES rails-diff! rake (~> 13.0) rspec (~> 3.0) + simplecov standard (~> 1.0) BUNDLED WITH diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 79b1be2..9691d91 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true +require "simplecov" +SimpleCov.start do + enable_coverage :branch + add_filter "/spec/" +end + require "rails/diff" Dir["./spec/support/**/*.rb"].sort.each { |f| require f } From 213663528319728fe6c489b583e1cec04d96cef0 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Fri, 13 Jun 2025 23:04:23 -0300 Subject: [PATCH 05/14] Make specs faster --- spec/lib/rails/diff/rails_repo_spec.rb | 59 +++++++++++++------------- spec/support/git_repo.rb | 32 ++++++++------ 2 files changed, 49 insertions(+), 42 deletions(-) diff --git a/spec/lib/rails/diff/rails_repo_spec.rb b/spec/lib/rails/diff/rails_repo_spec.rb index 1c50040..2f91a95 100644 --- a/spec/lib/rails/diff/rails_repo_spec.rb +++ b/spec/lib/rails/diff/rails_repo_spec.rb @@ -1,7 +1,7 @@ # Helper indices for commits: -# git_repo.commits[0] => 'add railties dir' (initial commit) -# git_repo.commits[1] => 'commit1' -# git_repo.commits.last => 'commit2' +# @git_repo.commits[0] => 'add railties dir' (initial commit) +# @git_repo.commits[1] => 'commit1' +# @git_repo.commits.last => 'commit2' require "rails/diff/rails_repo" require "fileutils" @@ -11,8 +11,9 @@ let(:logger) { spy } let(:cache_dir) { Dir.mktmpdir } let(:rails_path) { File.join(cache_dir, "rails") } - let(:git_repo) do - GitRepo.new.tap do |repo| + + before(:all) do + @git_repo = GitRepo.new.tap do |repo| repo.add_commit("commit1") repo.add_commit("commit2") end @@ -20,12 +21,12 @@ after do FileUtils.rm_rf(cache_dir) - git_repo.cleanup + @git_repo.cleanup end describe "#up_to_date?" do it "returns false when repo is not cloned yet" do - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) result = repo.up_to_date? @@ -33,8 +34,8 @@ end it "returns true when repo is on the latest commit" do - git_repo.clone_at_commit(git_repo.commits.last, rails_path) - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + @git_repo.clone_at_commit(@git_repo.commits.last, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) result = repo.up_to_date? @@ -42,8 +43,8 @@ end it "returns false and removes the repo when repo is on an old commit" do - git_repo.clone_at_commit(git_repo.commits.first, rails_path) - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + @git_repo.clone_at_commit(@git_repo.commits.first, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) result = repo.up_to_date? @@ -54,16 +55,16 @@ describe "#latest_commit" do it "returns the latest commit SHA from the remote repo" do - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) latest = repo.latest_commit - expect(latest).to eq git_repo.commits.last + expect(latest).to eq @git_repo.commits.last end it "returns the new latest commit after a new commit is pushed" do - new_commit = git_repo.add_commit("commit3") - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + new_commit = @git_repo.add_commit("commit3") + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) latest = repo.latest_commit @@ -73,32 +74,32 @@ describe "#checkout" do it "checks out the given commit in the repo" do - git_repo.clone_at_commit(git_repo.commits.last, rails_path) - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + @git_repo.clone_at_commit(@git_repo.commits.last, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) # Checkout the first file commit (not the initial railties commit) - repo.checkout(git_repo.commits[1]) + repo.checkout(@git_repo.commits[1]) # Verify HEAD is now at the first file commit current = Dir.chdir(rails_path) { `git rev-parse HEAD`.strip } - expect(current).to eq git_repo.commits[1] + expect(current).to eq @git_repo.commits[1] end it "logs the checkout info message" do - git_repo.clone_at_commit(git_repo.commits.last, rails_path) - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + @git_repo.clone_at_commit(@git_repo.commits.last, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) - repo.checkout(git_repo.commits[1]) + repo.checkout(@git_repo.commits[1]) expect(logger).to have_received(:info) - .with(/Checking out Rails \(at commit #{git_repo.commits[1][0..6]}\)/) + .with(/Checking out Rails \(at commit #{@git_repo.commits[1][0..6]}\)/) end end describe "#install_dependencies" do it "runs bundle check and bundle install if needed, and logs appropriately" do - git_repo.clone_at_commit(git_repo.commits[0], rails_path) - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + @git_repo.clone_at_commit(@git_repo.commits[0], rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) # Simulate bundle check failing, so bundle install is needed allow(Rails::Diff).to receive(:system!).with("bundle check", abort: false, logger: logger).and_return(false) @@ -111,8 +112,8 @@ end it "does not run bundle install if bundle check passes" do - git_repo.clone_at_commit(git_repo.commits[0], rails_path) - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + @git_repo.clone_at_commit(@git_repo.commits[0], rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) # Simulate bundle check passing allow(Rails::Diff).to receive(:system!).with("bundle check", abort: false, logger: logger).and_return(true) @@ -125,8 +126,8 @@ describe "#new_app" do it "runs the rails new command with the correct arguments and logs the command" do - git_repo.clone_at_commit(git_repo.commits.last, rails_path) - repo = described_class.new(logger:, cache_dir:, rails_repo: git_repo.remote_repo) + @git_repo.clone_at_commit(@git_repo.commits.last, rails_path) + repo = described_class.new(logger:, cache_dir:, rails_repo: @git_repo.remote_repo) allow(Rails::Diff).to receive(:system!).and_return(true) app_name = "myapp" diff --git a/spec/support/git_repo.rb b/spec/support/git_repo.rb index 1170c86..89770e1 100644 --- a/spec/support/git_repo.rb +++ b/spec/support/git_repo.rb @@ -8,33 +8,34 @@ class GitRepo def initialize @remote_dir = Dir.mktmpdir @remote_repo = File.join(@remote_dir, "origin.git") - Dir.chdir(@remote_dir) { `git init --bare origin.git > /dev/null 2>&1` } + Dir.chdir(@remote_dir) { `git init --bare --initial-branch=main origin.git > /dev/null 2>&1` } @commits = [] @work_dir = Dir.mktmpdir Dir.chdir(@work_dir) do `git clone #{@remote_repo} . > /dev/null 2>&1` `git checkout -b main > /dev/null 2>&1` - # Always create the 'railties' directory for test convenience FileUtils.mkdir_p("railties") - File.write("railties/.keep", "keep") + File.write("railties/README", "keep") `git add railties > /dev/null 2>&1` - `git commit -m "add railties dir" > /dev/null 2>&1` + + `git config user.email 'test@example.com'` + `git config user.name 'Test User'` + + commit_result = `git commit -m "add railties dir" 2>&1` + raise "Initial commit failed: #{commit_result}" unless $?.success? @commits << `git rev-parse HEAD`.strip - `git push origin main > /dev/null 2>&1` + `git push -u origin main > /dev/null 2>&1` end end - # Adds a commit with the given message and pushes to remote def add_commit(message) Dir.chdir(@work_dir) do - filename = "file#{@commits.size}.txt" - File.write(filename, message) - `git add . > /dev/null 2>&1` - `git commit -m "#{message}" > /dev/null 2>&1` + `git commit --allow-empty -n -m "#{message}" > /dev/null 2>&1` sha = `git rev-parse HEAD`.strip @commits << sha `git push origin main > /dev/null 2>&1` + `git fetch origin main > /dev/null 2>&1` end @commits.last @@ -42,11 +43,16 @@ def add_commit(message) def clone_at_commit(commit_sha, dest_path) `git clone #{@remote_repo} #{dest_path} > /dev/null 2>&1` - Dir.chdir(dest_path) { `git checkout #{commit_sha} > /dev/null 2>&1` } + Dir.chdir(dest_path) do + `git checkout #{commit_sha} > /dev/null 2>&1` + `git fetch origin main > /dev/null 2>&1` + end end def cleanup - FileUtils.rm_rf(@remote_dir) - FileUtils.rm_rf(@work_dir) + Dir.chdir(@work_dir) do + `git reset --hard main > /dev/null 2>&1` + `git clean -fdx > /dev/null 2>&1` + end end end From 0d70187c4cfa5f56cda9bbd288f22af8aa815801 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Jul 2025 01:30:06 +0000 Subject: [PATCH 06/14] Bump thor from 1.3.2 to 1.4.0 in the bundler group across 1 directory Bumps the bundler group with 1 update in the / directory: [thor](https://github.com/rails/thor). Updates `thor` from 1.3.2 to 1.4.0 - [Release notes](https://github.com/rails/thor/releases) - [Commits](https://github.com/rails/thor/compare/v1.3.2...v1.4.0) --- updated-dependencies: - dependency-name: thor dependency-version: 1.4.0 dependency-type: direct:production dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index a0f0a7a..f330619 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -242,7 +242,7 @@ GEM lint_roller (~> 1.1) rubocop-performance (~> 1.25.0) stringio (3.1.4) - thor (1.3.2) + thor (1.4.0) timeout (0.4.3) tzinfo (2.0.6) concurrent-ruby (~> 1.0) From 7bd3047f3e71de331b3cd96eded369e77d7bf2ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:18:31 +0000 Subject: [PATCH 07/14] Bump nokogiri in the bundler group across 1 directory Bumps the bundler group with 1 update in the / directory: [nokogiri](https://github.com/sparklemotion/nokogiri). Updates `nokogiri` from 1.18.8 to 1.18.9 - [Release notes](https://github.com/sparklemotion/nokogiri/releases) - [Changelog](https://github.com/sparklemotion/nokogiri/blob/main/CHANGELOG.md) - [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.18.8...v1.18.9) --- updated-dependencies: - dependency-name: nokogiri dependency-version: 1.18.9 dependency-type: indirect dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f330619..28433d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -117,7 +117,7 @@ GEM net-smtp marcel (1.0.4) mini_mime (1.1.5) - mini_portile2 (2.8.8) + mini_portile2 (2.8.9) minitest (5.25.4) net-imap (0.5.7) date @@ -129,10 +129,10 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.4) - nokogiri (1.18.8) + nokogiri (1.18.9) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.18.8-x86_64-darwin) + nokogiri (1.18.9-x86_64-darwin) racc (~> 1.4) parallel (1.27.0) parser (3.3.8.0) From 5574a47f7eb6eb6c88027abbc4154d44762a51d6 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Fri, 25 Jul 2025 13:13:18 -0300 Subject: [PATCH 08/14] Fix --fail-on-diff --- CHANGELOG.md | 1 + lib/rails/diff/cli.rb | 5 +++-- spec/lib/rails/diff/cli_spec.rb | 27 +++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 spec/lib/rails/diff/cli_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 68cbcc8..236aefb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] - Add `--only` option to only include specific files or directories in the diff. +- [BUGFIX] --fail-on-diff wasn't aborting with errors on diff. ## [0.5.0] - 2025-03-10 diff --git a/lib/rails/diff/cli.rb b/lib/rails/diff/cli.rb index 74a50bc..0e832b2 100644 --- a/lib/rails/diff/cli.rb +++ b/lib/rails/diff/cli.rb @@ -24,7 +24,8 @@ def file(*files) ) return if diff.empty? - options[:fail] ? abort(diff) : puts(diff) + options[:fail_on_diff] ? abort(diff) : puts(diff) + end end desc "generated GENERATOR [args]", "Compare files that would be created by a Rails generator" @@ -43,7 +44,7 @@ def generated(generator_name, *args) ) return if diff.empty? - options[:fail] ? abort(diff) : puts(diff) + options[:fail_on_diff] ? abort(diff) : puts(diff) end map %w[--version -v] => :__version diff --git a/spec/lib/rails/diff/cli_spec.rb b/spec/lib/rails/diff/cli_spec.rb new file mode 100644 index 0000000..c793c9a --- /dev/null +++ b/spec/lib/rails/diff/cli_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# require "spec_helper" + +RSpec.describe Rails::Diff::CLI do + describe "#file" do + context "when --fail-on-diff is not specified" do + it "exits successfully" do + allow(Rails::Diff).to receive(:file).with("some_file.rb", kind_of(Hash)).and_return("diff output") + + expect { + described_class.start(["file", "some_file.rb"]) + }.not_to raise_error + end + end + + context "when --fail-on-diff is specified" do + it "exits with an error code" do + allow(Rails::Diff).to receive(:file).with("some_file.rb", kind_of(Hash)).and_return("diff output") + + expect { + described_class.start(["file", "some_file.rb", "--fail-on-diff"]) + }.to raise_error(SystemExit) + end + end + end +end From 4e34111ba766aaf6174e9385b7f5e4efad6b0f2a Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Fri, 25 Jul 2025 13:14:40 -0300 Subject: [PATCH 09/14] Add rails-diff dotfiles command Co-authored-by: Steve Redka --- CHANGELOG.md | 1 + README.md | 9 ++++++++- lib/rails/diff/cli.rb | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 236aefb..52341f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## [Unreleased] - Add `--only` option to only include specific files or directories in the diff. +- Add `rails-diff dotfiles` to compare dotfiles (configuration files) in the repository. - [BUGFIX] --fail-on-diff wasn't aborting with errors on diff. ## [0.5.0] - 2025-03-10 diff --git a/README.md b/README.md index 0926e51..9deb309 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,14 @@ rails-diff generated scaffold Post --fail-on-diff rails-diff generated authentication --commit 7df1b8 ``` -### Options +### Compare dotfiles (configuration files) + +```bash +# Compare configuration files like .gitignore, .rspec, .rubocop.yml +rails-diff dotfiles +``` + +### Global Options #### --fail-on-diff diff --git a/lib/rails/diff/cli.rb b/lib/rails/diff/cli.rb index 0e832b2..15dad58 100644 --- a/lib/rails/diff/cli.rb +++ b/lib/rails/diff/cli.rb @@ -26,6 +26,12 @@ def file(*files) options[:fail_on_diff] ? abort(diff) : puts(diff) end + + desc "dotfiles", "Compare dotfiles in your repository with the ones generated by Rails" + def dotfiles + dotfiles = `git ls-files --cached --others --exclude-standard -- '.*'`.split("\n") + + file(*dotfiles) end desc "generated GENERATOR [args]", "Compare files that would be created by a Rails generator" From cfe4dffdb0a5599ada209eb5c5717d2f99802447 Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Fri, 25 Jul 2025 13:19:13 -0300 Subject: [PATCH 10/14] v0.6.0 --- CHANGELOG.md | 5 +++++ lib/rails/diff/version.rb | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52341f5..662ecc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## [Unreleased] +## [0.6.0] - 2025-07-25 + - Add `--only` option to only include specific files or directories in the diff. - Add `rails-diff dotfiles` to compare dotfiles (configuration files) in the repository. - [BUGFIX] --fail-on-diff wasn't aborting with errors on diff. @@ -68,6 +70,9 @@ M## [0.1.1] - 2025-02-21 - Initial release +[0.6.0]: https://github.com/matheusrich/rails-diff/releases/tag/v0.6.0 +[0.5.0]: https://github.com/matheusrich/rails-diff/releases/tag/v0.5.0 +[0.4.1]: https://github.com/matheusrich/rails-diff/releases/tag/v0.4.1 [0.4.1]: https://github.com/matheusrich/rails-diff/releases/tag/v0.4.1 [0.4.0]: https://github.com/matheusrich/rails-diff/releases/tag/v0.4.0 [0.3.0]: https://github.com/matheusrich/rails-diff/releases/tag/v0.3.0 diff --git a/lib/rails/diff/version.rb b/lib/rails/diff/version.rb index 2e6fa4a..4da0b83 100644 --- a/lib/rails/diff/version.rb +++ b/lib/rails/diff/version.rb @@ -2,6 +2,6 @@ module Rails module Diff - VERSION = "0.5.0" + VERSION = "0.6.0" end end From b82169e6440f098fffd2f928dc83bb4b62bf38ff Mon Sep 17 00:00:00 2001 From: Matheus Richard Date: Fri, 25 Jul 2025 15:30:43 -0300 Subject: [PATCH 11/14] Bundle --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 28433d7..d77489f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - rails-diff (0.5.0) + rails-diff (0.6.0) diffy (~> 3.4) rails (>= 7.0) thor (~> 1.0) From 288c9c6547c0b85abc21048b7adfb78b49e8a1cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 22:35:47 +0000 Subject: [PATCH 12/14] Bump activerecord in the bundler group across 1 directory Bumps the bundler group with 1 update in the / directory: [activerecord](https://github.com/rails/rails). Updates `activerecord` from 8.0.1 to 8.0.2.1 - [Release notes](https://github.com/rails/rails/releases) - [Changelog](https://github.com/rails/rails/blob/v8.0.2.1/activerecord/CHANGELOG.md) - [Commits](https://github.com/rails/rails/compare/v8.0.1...v8.0.2.1) --- updated-dependencies: - dependency-name: activerecord dependency-version: 8.0.2.1 dependency-type: indirect dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 120 +++++++++++++++++++++++++-------------------------- 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d77489f..578b4f2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,29 +9,29 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (8.0.1) - actionpack (= 8.0.1) - activesupport (= 8.0.1) + actioncable (8.0.2.1) + actionpack (= 8.0.2.1) + activesupport (= 8.0.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (8.0.1) - actionpack (= 8.0.1) - activejob (= 8.0.1) - activerecord (= 8.0.1) - activestorage (= 8.0.1) - activesupport (= 8.0.1) + actionmailbox (8.0.2.1) + actionpack (= 8.0.2.1) + activejob (= 8.0.2.1) + activerecord (= 8.0.2.1) + activestorage (= 8.0.2.1) + activesupport (= 8.0.2.1) mail (>= 2.8.0) - actionmailer (8.0.1) - actionpack (= 8.0.1) - actionview (= 8.0.1) - activejob (= 8.0.1) - activesupport (= 8.0.1) + actionmailer (8.0.2.1) + actionpack (= 8.0.2.1) + actionview (= 8.0.2.1) + activejob (= 8.0.2.1) + activesupport (= 8.0.2.1) mail (>= 2.8.0) rails-dom-testing (~> 2.2) - actionpack (8.0.1) - actionview (= 8.0.1) - activesupport (= 8.0.1) + actionpack (8.0.2.1) + actionview (= 8.0.2.1) + activesupport (= 8.0.2.1) nokogiri (>= 1.8.5) rack (>= 2.2.4) rack-session (>= 1.0.1) @@ -39,35 +39,35 @@ GEM rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) useragent (~> 0.16) - actiontext (8.0.1) - actionpack (= 8.0.1) - activerecord (= 8.0.1) - activestorage (= 8.0.1) - activesupport (= 8.0.1) + actiontext (8.0.2.1) + actionpack (= 8.0.2.1) + activerecord (= 8.0.2.1) + activestorage (= 8.0.2.1) + activesupport (= 8.0.2.1) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (8.0.1) - activesupport (= 8.0.1) + actionview (8.0.2.1) + activesupport (= 8.0.2.1) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (8.0.1) - activesupport (= 8.0.1) + activejob (8.0.2.1) + activesupport (= 8.0.2.1) globalid (>= 0.3.6) - activemodel (8.0.1) - activesupport (= 8.0.1) - activerecord (8.0.1) - activemodel (= 8.0.1) - activesupport (= 8.0.1) + activemodel (8.0.2.1) + activesupport (= 8.0.2.1) + activerecord (8.0.2.1) + activemodel (= 8.0.2.1) + activesupport (= 8.0.2.1) timeout (>= 0.4.0) - activestorage (8.0.1) - actionpack (= 8.0.1) - activejob (= 8.0.1) - activerecord (= 8.0.1) - activesupport (= 8.0.1) + activestorage (8.0.2.1) + actionpack (= 8.0.2.1) + activejob (= 8.0.2.1) + activerecord (= 8.0.2.1) + activesupport (= 8.0.2.1) marcel (~> 1.0) - activesupport (8.0.1) + activesupport (8.0.2.1) base64 benchmark (>= 0.3) bigdecimal @@ -81,18 +81,18 @@ GEM tzinfo (~> 2.0, >= 2.0.5) uri (>= 0.13.1) ast (2.4.3) - base64 (0.2.0) - benchmark (0.4.0) - bigdecimal (3.1.9) + base64 (0.3.0) + benchmark (0.4.1) + bigdecimal (3.2.2) builder (3.3.0) concurrent-ruby (1.3.5) - connection_pool (2.5.0) + connection_pool (2.5.3) crass (1.0.6) date (3.4.1) diff-lcs (1.5.1) diffy (3.4.3) docile (1.4.1) - drb (2.2.1) + drb (2.2.3) erubi (1.13.1) globalid (1.2.1) activesupport (>= 6.1) @@ -106,7 +106,7 @@ GEM json (2.12.2) language_server-protocol (3.17.0.5) lint_roller (1.1.0) - logger (1.6.6) + logger (1.7.0) loofah (2.24.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -118,7 +118,7 @@ GEM marcel (1.0.4) mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (5.25.4) + minitest (5.25.5) net-imap (0.5.7) date net-protocol @@ -154,20 +154,20 @@ GEM rack (>= 1.3) rackup (2.2.1) rack (>= 3) - rails (8.0.1) - actioncable (= 8.0.1) - actionmailbox (= 8.0.1) - actionmailer (= 8.0.1) - actionpack (= 8.0.1) - actiontext (= 8.0.1) - actionview (= 8.0.1) - activejob (= 8.0.1) - activemodel (= 8.0.1) - activerecord (= 8.0.1) - activestorage (= 8.0.1) - activesupport (= 8.0.1) + rails (8.0.2.1) + actioncable (= 8.0.2.1) + actionmailbox (= 8.0.2.1) + actionmailer (= 8.0.2.1) + actionpack (= 8.0.2.1) + actiontext (= 8.0.2.1) + actionview (= 8.0.2.1) + activejob (= 8.0.2.1) + activemodel (= 8.0.2.1) + activerecord (= 8.0.2.1) + activestorage (= 8.0.2.1) + activesupport (= 8.0.2.1) bundler (>= 1.15.0) - railties (= 8.0.1) + railties (= 8.0.2.1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -175,9 +175,9 @@ GEM rails-html-sanitizer (1.6.2) loofah (~> 2.21) nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) - railties (8.0.1) - actionpack (= 8.0.1) - activesupport (= 8.0.1) + railties (8.0.2.1) + actionpack (= 8.0.2.1) + activesupport (= 8.0.2.1) irb (~> 1.13) rackup (>= 1.0.0) rake (>= 12.2) From bd054d3c5000930ae8ce057baa5aa31bf9702f8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:20:02 +0000 Subject: [PATCH 13/14] Bump rack from 3.1.16 to 3.1.17 in the bundler group across 1 directory Bumps the bundler group with 1 update in the / directory: [rack](https://github.com/rack/rack). Updates `rack` from 3.1.16 to 3.1.17 - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/v3.1.16...v3.1.17) --- updated-dependencies: - dependency-name: rack dependency-version: 3.1.17 dependency-type: indirect dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 578b4f2..56116b7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,7 +146,7 @@ GEM date stringio racc (1.8.1) - rack (3.1.16) + rack (3.1.17) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0) From c7891be4b9e639eba726c7e677604a114cb425b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 18:30:46 +0000 Subject: [PATCH 14/14] Bump rack from 3.1.17 to 3.1.18 in the bundler group across 1 directory Bumps the bundler group with 1 update in the / directory: [rack](https://github.com/rack/rack). Updates `rack` from 3.1.17 to 3.1.18 - [Release notes](https://github.com/rack/rack/releases) - [Changelog](https://github.com/rack/rack/blob/main/CHANGELOG.md) - [Commits](https://github.com/rack/rack/compare/v3.1.17...v3.1.18) --- updated-dependencies: - dependency-name: rack dependency-version: 3.1.18 dependency-type: indirect dependency-group: bundler ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 56116b7..d49748b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,7 +146,7 @@ GEM date stringio racc (1.8.1) - rack (3.1.17) + rack (3.1.18) rack-session (2.1.1) base64 (>= 0.1.0) rack (>= 3.0.0)