Class: FormatStaged::Job

Inherits:
Object
  • Object
show all
Includes:
IOMixin
Defined in:
lib/format-staged/job.rb

Overview

Runs staged changes through a formatting tool

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from IOMixin

#fail!, #get_output, #get_status, #info, #pipe_command, #read_output, #verbose_info, #warning

Constructor Details

#initialize(formatter:, patterns:, **options) ⇒ Job

Returns a new instance of Job.



18
19
20
21
22
23
24
25
26
27
28
# File 'lib/format-staged/job.rb', line 18

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

Instance Attribute Details

#formatterObject (readonly)

Returns the value of attribute formatter.



16
17
18
# File 'lib/format-staged/job.rb', line 16

def formatter
  @formatter
end

#patternsObject (readonly)

Returns the value of attribute patterns.



16
17
18
# File 'lib/format-staged/job.rb', line 16

def patterns
  @patterns
end

#updateObject (readonly)

Returns the value of attribute update.



16
17
18
# File 'lib/format-staged/job.rb', line 16

def update
  @update
end

#verboseObject (readonly)

Returns the value of attribute verbose.



16
17
18
# File 'lib/format-staged/job.rb', line 16

def verbose
  @verbose
end

#writeObject (readonly)

Returns the value of attribute write.



16
17
18
# File 'lib/format-staged/job.rb', line 16

def write
  @write
end

Instance Method Details

#format_file(file) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/format-staged/job.rb', line 62

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 true
  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

  if new_hash == file.src_hash
    info "File #{file.src_path} equal to comitted"
    return true
  end

  true
end

#format_object(file) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/format-staged/job.rb', line 98

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

  write_args = write ? ['-w'] : []
  pid3, r = pipe_command 'git', 'hash-object', *write_args, '--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

#matching_files(root) ⇒ Object



53
54
55
56
57
58
59
60
# File 'lib/format-staged/job.rb', line 53

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

#object_is_empty(hash) ⇒ Object



123
124
125
126
# File 'lib/format-staged/job.rb', line 123

def object_is_empty(hash)
  size = get_output('git', 'cat-file', '-s', hash).first.to_i
  size.zero?
end

#patch_working_file(file, new_hash) ⇒ Object



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/format-staged/job.rb', line 128

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

#replace_file_in_index(file, new_hash) ⇒ Object



147
148
149
# File 'lib/format-staged/job.rb', line 147

def replace_file_in_index(file, new_hash)
  get_output 'git', 'update-index', '--cacheinfo', "#{file.dst_mode},#{new_hash},#{file.src_path}"
end

#repo_rootObject



45
46
47
48
49
50
51
# File 'lib/format-staged/job.rb', line 45

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

#runObject



30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/format-staged/job.rb', line 30

def run
  files = matching_files(repo_root)
  if files.empty?
    info 'No staged file matching pattern. Done'
    return true
  end

  formatted = files.filter { |file| format_file file }

  return false unless formatted.size == files.size

  quiet = @verbose ? [] : ['--quiet']
  return !write || get_status('git', 'diff-index', '--cached', '--exit-code', *quiet, 'HEAD') != 0
end

#update_working_copy(file, new_hash) ⇒ Object



88
89
90
91
92
93
94
95
96
# File 'lib/format-staged/job.rb', line 88

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

#validate_patterns(patterns) ⇒ Object



151
152
153
154
155
# File 'lib/format-staged/job.rb', line 151

def validate_patterns(patterns)
  patterns.each do |pattern|
    fail! "Negative pattern '#{pattern}' is not yet supported" if pattern.start_with? '!'
  end
end