Create top-level module
This commit is contained in:
parent
6fd8bb9ce7
commit
9fd78ac2e1
6 changed files with 200 additions and 182 deletions
|
@ -73,5 +73,4 @@ if !parameters[:formatter] || parameters[:patterns].empty?
|
|||
exit
|
||||
end
|
||||
|
||||
formatter = FormatStaged.new(**parameters)
|
||||
formatter.run
|
||||
FormatStaged.run(**parameters)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FormatStaged
|
||||
module FormatStaged
|
||||
##
|
||||
# Entry in the git index.
|
||||
#
|
||||
|
|
|
@ -2,61 +2,65 @@
|
|||
|
||||
require 'English'
|
||||
|
||||
class FormatStaged
|
||||
def get_output(*args, lines: true, silent: false)
|
||||
puts "> #{args.join(' ')}" if @verbose
|
||||
module FormatStaged
|
||||
##
|
||||
# Mixin that provides IO methods
|
||||
module IOMixin
|
||||
def get_output(*args, lines: true, silent: false)
|
||||
puts "> #{args.join(' ')}" if @verbose
|
||||
|
||||
r = IO.popen(args, err: :err)
|
||||
output = read_output(r, lines: lines, silent: silent)
|
||||
r = IO.popen(args, err: :err)
|
||||
output = read_output(r, lines: lines, silent: silent)
|
||||
|
||||
raise 'Failed to run command' unless $CHILD_STATUS.success?
|
||||
raise 'Failed to run command' unless $CHILD_STATUS.success?
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
def pipe_command(*args, source: nil)
|
||||
puts (source.nil? ? '> ' : '| ') + args.join(' ') if @verbose
|
||||
r, w = IO.pipe
|
||||
|
||||
opts = {}
|
||||
opts[:in] = source unless source.nil?
|
||||
opts[:out] = w
|
||||
opts[:err] = :err
|
||||
|
||||
pid = spawn(*args, **opts)
|
||||
|
||||
w.close
|
||||
source&.close
|
||||
|
||||
[pid, r]
|
||||
end
|
||||
|
||||
def read_output(output, lines: true, silent: false)
|
||||
result = output.read
|
||||
splits = result.split("\n")
|
||||
if @verbose && !silent
|
||||
splits.each do |line|
|
||||
puts "< #{line}"
|
||||
end
|
||||
output
|
||||
end
|
||||
output.close
|
||||
|
||||
lines ? splits : result
|
||||
end
|
||||
def pipe_command(*args, source: nil)
|
||||
puts (source.nil? ? '> ' : '| ') + args.join(' ') if @verbose
|
||||
r, w = IO.pipe
|
||||
|
||||
def fail!(message)
|
||||
abort "💣 #{message.red}"
|
||||
end
|
||||
opts = {}
|
||||
opts[:in] = source unless source.nil?
|
||||
opts[:out] = w
|
||||
opts[:err] = :err
|
||||
|
||||
def warning(message)
|
||||
warn "⚠️ #{message.yellow}"
|
||||
end
|
||||
pid = spawn(*args, **opts)
|
||||
|
||||
def info(message)
|
||||
puts message.blue
|
||||
end
|
||||
w.close
|
||||
source&.close
|
||||
|
||||
def verbose_info(message)
|
||||
puts "ℹ️ #{message}" if verbose
|
||||
[pid, r]
|
||||
end
|
||||
|
||||
def read_output(output, lines: true, silent: false)
|
||||
result = output.read
|
||||
splits = result.split("\n")
|
||||
if @verbose && !silent
|
||||
splits.each do |line|
|
||||
puts "< #{line}"
|
||||
end
|
||||
end
|
||||
output.close
|
||||
|
||||
lines ? splits : result
|
||||
end
|
||||
|
||||
def fail!(message)
|
||||
abort "💣 #{message.red}"
|
||||
end
|
||||
|
||||
def warning(message)
|
||||
warn "⚠️ #{message.yellow}"
|
||||
end
|
||||
|
||||
def info(message)
|
||||
puts message.blue
|
||||
end
|
||||
|
||||
def verbose_info(message)
|
||||
puts "ℹ️ #{message}" if verbose
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
141
lib/format-staged/job.rb
Normal file
141
lib/format-staged/job.rb
Normal file
|
@ -0,0 +1,141 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'entry'
|
||||
require_relative 'io'
|
||||
|
||||
require 'shellwords'
|
||||
require 'colorize'
|
||||
require 'English'
|
||||
|
||||
module FormatStaged
|
||||
##
|
||||
# Runs staged changes through a formatting tool
|
||||
class Job
|
||||
include IOMixin
|
||||
|
||||
attr_reader :formatter, :patterns, :update, :write, :verbose
|
||||
|
||||
def initialize(formatter:, patterns:, **options)
|
||||
validate_patterns patterns
|
||||
|
||||
@formatter = formatter
|
||||
@patterns = patterns
|
||||
@update = options.fetch(:update, true)
|
||||
@write = options.fetch(:write, true)
|
||||
@verbose = options.fetch(:verbose, true)
|
||||
|
||||
String.disable_colorization = !options.fetch(:color_output, $stdout.isatty)
|
||||
end
|
||||
|
||||
def run
|
||||
matching_files(repo_root).each do |file|
|
||||
format_file(file)
|
||||
end
|
||||
end
|
||||
|
||||
def repo_root
|
||||
verbose_info 'Finding repository root'
|
||||
root = get_output('git', 'rev-parse', '--show-toplevel', lines: false).chomp
|
||||
verbose_info "Repo at #{root}"
|
||||
|
||||
root
|
||||
end
|
||||
|
||||
def matching_files(root)
|
||||
verbose_info 'Listing staged files'
|
||||
|
||||
get_output('git', 'diff-index', '--cached', '--diff-filter=AM', '--no-renames', 'HEAD')
|
||||
.map { |line| Entry.new(line, root: root) }
|
||||
.reject(&:symlink?)
|
||||
.filter { |entry| entry.matches?(@patterns) }
|
||||
end
|
||||
|
||||
def format_file(file)
|
||||
new_hash = format_object file
|
||||
|
||||
return true unless write
|
||||
|
||||
if new_hash == file.dst_hash
|
||||
info "Unchanged #{file.src_path}"
|
||||
return false
|
||||
end
|
||||
|
||||
if object_is_empty new_hash
|
||||
info "Skipping #{file.src_path}, formatted file is empty"
|
||||
return false
|
||||
end
|
||||
|
||||
replace_file_in_index file, new_hash
|
||||
update_working_copy file, new_hash
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def update_working_copy(file, new_hash)
|
||||
return unless update
|
||||
|
||||
begin
|
||||
patch_working_file file, new_hash
|
||||
rescue StandardError => e
|
||||
warning "failed updating #{file.src_path} in working copy: #{e}"
|
||||
end
|
||||
end
|
||||
|
||||
def format_object(file)
|
||||
info "Formatting #{file.src_path}"
|
||||
|
||||
format_command = formatter.sub('{}', file.src_path.shellescape)
|
||||
|
||||
pid1, r = pipe_command 'git', 'cat-file', '-p', file.dst_hash
|
||||
pid2, r = pipe_command format_command, source: r
|
||||
pid3, r = pipe_command 'git', 'hash-object', '-w', '--stdin', source: r
|
||||
|
||||
result = read_output(r, lines: false).chomp
|
||||
|
||||
Process.wait pid1
|
||||
raise "Cannot read #{file.dst_hash} from object database" unless $CHILD_STATUS.success?
|
||||
|
||||
Process.wait pid2
|
||||
raise "Error formatting #{file.src_path}" unless $CHILD_STATUS.success?
|
||||
|
||||
Process.wait pid3
|
||||
raise 'Error writing formatted file back to object database' unless $CHILD_STATUS.success? && !result.empty?
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def object_is_empty(hash)
|
||||
size = get_output('git', 'cat-file', '-s', hash).first.to_i
|
||||
size.zero?
|
||||
end
|
||||
|
||||
def patch_working_file(file, new_hash)
|
||||
info 'Updating working copy'
|
||||
|
||||
patch = get_output 'git', 'diff', file.dst_hash, new_hash, lines: false, silent: true
|
||||
patch.gsub! "a/#{file.dst_hash}", "a/#{file.src_path}"
|
||||
patch.gsub! "b/#{new_hash}", "b/#{file.src_path}"
|
||||
|
||||
input, patch_out = IO.pipe
|
||||
pid, r = pipe_command 'git', 'apply', '-', source: input
|
||||
|
||||
patch_out.write patch
|
||||
patch_out.close
|
||||
|
||||
read_output r
|
||||
|
||||
Process.wait pid
|
||||
raise 'Error applying patch' unless $CHILD_STATUS.success?
|
||||
end
|
||||
|
||||
def replace_file_in_index(file, new_hash)
|
||||
get_output 'git', 'update-index', '--cacheinfo', "#{file.dst_mode},#{new_hash},#{file.src_path}"
|
||||
end
|
||||
|
||||
def validate_patterns(patterns)
|
||||
patterns.each do |pattern|
|
||||
fail! "Negative pattern '#{pattern}' is not yet supported" if pattern.start_with? '!'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FormatStaged
|
||||
module FormatStaged
|
||||
VERSION = '0.0.3'
|
||||
end
|
||||
|
|
|
@ -1,137 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'English'
|
||||
require 'format-staged/version'
|
||||
require 'format-staged/entry'
|
||||
require 'format-staged/io'
|
||||
require 'shellwords'
|
||||
require 'colorize'
|
||||
require_relative 'format-staged/job'
|
||||
|
||||
##
|
||||
# Runs staged changes through a formatting tool
|
||||
class FormatStaged
|
||||
attr_reader :formatter, :patterns, :update, :write, :verbose
|
||||
|
||||
def initialize(formatter:, patterns:, **options)
|
||||
validate_patterns patterns
|
||||
|
||||
@formatter = formatter
|
||||
@patterns = patterns
|
||||
@update = options.fetch(:update, true)
|
||||
@write = options.fetch(:write, true)
|
||||
@verbose = options.fetch(:verbose, true)
|
||||
|
||||
String.disable_colorization = !options.fetch(:color_output, $stdout.isatty)
|
||||
end
|
||||
|
||||
def run
|
||||
matching_files(repo_root).each do |file|
|
||||
format_file(file)
|
||||
end
|
||||
end
|
||||
|
||||
def repo_root
|
||||
verbose_info 'Finding repository root'
|
||||
root = get_output('git', 'rev-parse', '--show-toplevel', lines: false).chomp
|
||||
verbose_info "Repo at #{root}"
|
||||
|
||||
root
|
||||
end
|
||||
|
||||
def matching_files(root)
|
||||
verbose_info 'Listing staged files'
|
||||
|
||||
get_output('git', 'diff-index', '--cached', '--diff-filter=AM', '--no-renames', 'HEAD')
|
||||
.map { |line| Entry.new(line, root: root) }
|
||||
.reject(&:symlink?)
|
||||
.filter { |entry| entry.matches?(@patterns) }
|
||||
end
|
||||
|
||||
def format_file(file)
|
||||
new_hash = format_object file
|
||||
|
||||
return true unless write
|
||||
|
||||
if new_hash == file.dst_hash
|
||||
info "Unchanged #{file.src_path}"
|
||||
return false
|
||||
end
|
||||
|
||||
if object_is_empty new_hash
|
||||
info "Skipping #{file.src_path}, formatted file is empty"
|
||||
return false
|
||||
end
|
||||
|
||||
replace_file_in_index file, new_hash
|
||||
update_working_copy file, new_hash
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def update_working_copy(file, new_hash)
|
||||
return unless update
|
||||
|
||||
begin
|
||||
patch_working_file file, new_hash
|
||||
rescue StandardError => e
|
||||
warning "failed updating #{file.src_path} in working copy: #{e}"
|
||||
end
|
||||
end
|
||||
|
||||
def format_object(file)
|
||||
info "Formatting #{file.src_path}"
|
||||
|
||||
format_command = formatter.sub('{}', file.src_path.shellescape)
|
||||
|
||||
pid1, r = pipe_command 'git', 'cat-file', '-p', file.dst_hash
|
||||
pid2, r = pipe_command format_command, source: r
|
||||
pid3, r = pipe_command 'git', 'hash-object', '-w', '--stdin', source: r
|
||||
|
||||
result = read_output(r, lines: false).chomp
|
||||
|
||||
Process.wait pid1
|
||||
raise "Cannot read #{file.dst_hash} from object database" unless $CHILD_STATUS.success?
|
||||
|
||||
Process.wait pid2
|
||||
raise "Error formatting #{file.src_path}" unless $CHILD_STATUS.success?
|
||||
|
||||
Process.wait pid3
|
||||
raise 'Error writing formatted file back to object database' unless $CHILD_STATUS.success? && !result.empty?
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def object_is_empty(hash)
|
||||
size = get_output('git', 'cat-file', '-s', hash).first.to_i
|
||||
size.zero?
|
||||
end
|
||||
|
||||
def patch_working_file(file, new_hash)
|
||||
info 'Updating working copy'
|
||||
|
||||
patch = get_output 'git', 'diff', file.dst_hash, new_hash, lines: false, silent: true
|
||||
patch.gsub! "a/#{file.dst_hash}", "a/#{file.src_path}"
|
||||
patch.gsub! "b/#{new_hash}", "b/#{file.src_path}"
|
||||
|
||||
input, patch_out = IO.pipe
|
||||
pid, r = pipe_command 'git', 'apply', '-', source: input
|
||||
|
||||
patch_out.write patch
|
||||
patch_out.close
|
||||
|
||||
read_output r
|
||||
|
||||
Process.wait pid
|
||||
raise 'Error applying patch' unless $CHILD_STATUS.success?
|
||||
end
|
||||
|
||||
def replace_file_in_index(file, new_hash)
|
||||
get_output 'git', 'update-index', '--cacheinfo', "#{file.dst_mode},#{new_hash},#{file.src_path}"
|
||||
end
|
||||
|
||||
def validate_patterns(patterns)
|
||||
patterns.each do |pattern|
|
||||
fail! "Negative pattern '#{pattern}' is not yet supported" if pattern.start_with? '!'
|
||||
end
|
||||
# FormatStaged module
|
||||
module FormatStaged
|
||||
def self.run(**options)
|
||||
Job.new(**options).run
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Reference in a new issue