format-staged 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 80bea79a03480ca702efd9bec176a0a2e180e82e854c7300773b064bf4c46b1e
4
- data.tar.gz: 06356777c82f354560e700aef495929f1dc7e4d0d72c08456271a3b08fd3d8cc
3
+ metadata.gz: de70125ce0e96109a9985370a252e9746dc9a87cb16bd49e6ac669251b6f0fa9
4
+ data.tar.gz: d06a10b6260cd39474a0b3854f1d6458f102c975cdc62251a97606864693dd32
5
5
  SHA512:
6
- metadata.gz: bd2a7a026faf4ff75b34b69a26aba8df9ff9d18d15be75341c365a8c5bae010044010d2fe3976cd5b60ccc9e7f0d68c7a62955bc620c56b7ad3955779713ec95
7
- data.tar.gz: 126334604bfac8205438d64d597638490d560db058476f4a53b673f89f2b80cbc055a3f76863d5f6489b89f132140947794cb4a0cece89ea89ea287fe6c0c5f8
6
+ metadata.gz: 6fe36e8bb0256f4043c2314d19a21855321204aa40f41fe07a9cd00a64f50d2090c8a90def65f4e52a332dc049b625e96e921f948908f5fe04352c50f2a71d70
7
+ data.tar.gz: e576b4a239f62ac50a4d231b74924ee7186f7abbd86c9faadbad0aa3c090ccbc876b1e57197f7f2681edd945b8d038ba4f1d9a9b16eedd1cab90e5c24ae36e9a
@@ -57,6 +57,10 @@ parser = OptionParser.new do |opt|
57
57
  puts FormatStaged::VERSION
58
58
  exit
59
59
  end
60
+
61
+ opt.on('--[no-]color', 'Colorizes output') do |value|
62
+ parameters[:color_output] = value
63
+ end
60
64
  end
61
65
 
62
66
  parser.parse!
@@ -69,5 +73,10 @@ if !parameters[:formatter] || parameters[:patterns].empty?
69
73
  exit
70
74
  end
71
75
 
72
- formatter = FormatStaged.new(**parameters)
73
- formatter.run
76
+ begin
77
+ success = FormatStaged.run(**parameters)
78
+ exit success ? 0 : 1
79
+ rescue RuntimeError => e
80
+ puts "💣 Error: #{e.message}".red
81
+ exit 1
82
+ end
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class FormatStaged
3
+ module FormatStaged
4
+ ##
5
+ # Entry in the git index.
6
+ #
7
+ # Data as produced by `git diff-index`
4
8
  class Entry
5
9
  PATTERN = /^
6
10
  :(?<src_mode>\d+)\s
@@ -1,43 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'English'
4
- class FormatStaged
5
- def get_output(*args, lines: true)
6
- puts "> #{args.join(' ')}" if @verbose
7
-
8
- output = IO.popen(args, err: :err) do |io|
9
- if lines
10
- io.readlines.map(&:chomp)
11
- else
12
- io.read
13
- end
4
+
5
+ module FormatStaged
6
+ ##
7
+ # Mixin that provides IO methods
8
+ module IOMixin
9
+ def get_output(*args, lines: true, silent: false)
10
+ puts "> #{args.join(' ')}" if @verbose
11
+
12
+ r = IO.popen(args, err: :err)
13
+ output = read_output(r, lines: lines, silent: silent)
14
+
15
+ raise 'Failed to run command' unless $CHILD_STATUS.success?
16
+
17
+ output
14
18
  end
15
19
 
16
- if @verbose && lines
17
- output.each do |line|
18
- puts "< #{line}"
19
- end
20
+ def get_status(*args)
21
+ puts "> #{args.join(' ')}" if @verbose
22
+ result = system(*args)
23
+
24
+ raise 'Failed to run command' if result.nil?
25
+
26
+ puts "? #{$CHILD_STATUS.exitstatus}" if @verbose
27
+
28
+ $CHILD_STATUS.exitstatus
20
29
  end
21
30
 
22
- raise 'Failed to run command' unless $CHILD_STATUS.success?
31
+ def pipe_command(*args, source: nil)
32
+ puts (source.nil? ? '> ' : '| ') + args.join(' ') if @verbose
33
+ r, w = IO.pipe
23
34
 
24
- output
25
- end
35
+ opts = {}
36
+ opts[:in] = source unless source.nil?
37
+ opts[:out] = w
38
+ opts[:err] = :err
26
39
 
27
- def pipe_command(*args, source: nil)
28
- puts (source.nil? ? '> ' : '| ') + args.join(' ') if @verbose
29
- r, w = IO.pipe
40
+ pid = spawn(*args, **opts)
30
41
 
31
- opts = {}
32
- opts[:in] = source unless source.nil?
33
- opts[:out] = w
34
- opts[:err] = :err
42
+ w.close
43
+ source&.close
35
44
 
36
- pid = spawn(*args, **opts)
45
+ [pid, r]
46
+ end
37
47
 
38
- w.close
39
- source&.close
48
+ def read_output(output, lines: true, silent: false)
49
+ result = output.read
50
+ splits = result.split("\n")
51
+ if @verbose && !silent
52
+ splits.each do |line|
53
+ puts "< #{line}"
54
+ end
55
+ end
56
+ output.close
57
+
58
+ lines ? splits : result
59
+ end
60
+
61
+ def fail!(message)
62
+ abort "💣 #{message.red}"
63
+ end
40
64
 
41
- [pid, r]
65
+ def warning(message)
66
+ warn "⚠️ #{message.yellow}"
67
+ end
68
+
69
+ def info(message)
70
+ puts message.blue
71
+ end
72
+
73
+ def verbose_info(message)
74
+ puts "ℹ️ #{message}" if verbose
75
+ end
42
76
  end
43
77
  end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'entry'
4
+ require_relative 'io'
5
+
6
+ require 'shellwords'
7
+ require 'colorize'
8
+ require 'English'
9
+
10
+ module FormatStaged
11
+ ##
12
+ # Runs staged changes through a formatting tool
13
+ class Job
14
+ include IOMixin
15
+
16
+ attr_reader :formatter, :patterns, :update, :write, :verbose
17
+
18
+ def initialize(formatter:, patterns:, **options)
19
+ validate_patterns patterns
20
+
21
+ @formatter = formatter
22
+ @patterns = patterns
23
+ @update = options.fetch(:update, true)
24
+ @write = options.fetch(:write, true)
25
+ @verbose = options.fetch(:verbose, true)
26
+
27
+ String.disable_colorization = !options.fetch(:color_output, $stdout.isatty)
28
+ end
29
+
30
+ def run
31
+ files = matching_files(repo_root)
32
+ if files.empty?
33
+ info 'No staged file matching pattern. Done'
34
+ return true
35
+ end
36
+
37
+ formatted = files.filter { |file| format_file file }
38
+
39
+ return false unless formatted.size == files.size
40
+
41
+ quiet = @verbose ? [] : ['--quiet']
42
+ return !write || get_status('git', 'diff-index', '--cached', '--exit-code', *quiet, 'HEAD') != 0
43
+ end
44
+
45
+ def repo_root
46
+ verbose_info 'Finding repository root'
47
+ root = get_output('git', 'rev-parse', '--show-toplevel', lines: false).chomp
48
+ verbose_info "Repo at #{root}"
49
+
50
+ root
51
+ end
52
+
53
+ def matching_files(root)
54
+ verbose_info 'Listing staged files'
55
+
56
+ get_output('git', 'diff-index', '--cached', '--diff-filter=AM', '--no-renames', 'HEAD')
57
+ .map { |line| Entry.new(line, root: root) }
58
+ .reject(&:symlink?)
59
+ .filter { |entry| entry.matches?(@patterns) }
60
+ end
61
+
62
+ def format_file(file)
63
+ new_hash = format_object file
64
+
65
+ return true unless write
66
+
67
+ if new_hash == file.dst_hash
68
+ info "Unchanged #{file.src_path}"
69
+ return true
70
+ end
71
+
72
+ if object_is_empty new_hash
73
+ info "Skipping #{file.src_path}, formatted file is empty"
74
+ return false
75
+ end
76
+
77
+ replace_file_in_index file, new_hash
78
+ update_working_copy file, new_hash
79
+
80
+ if new_hash == file.src_hash
81
+ info "File #{file.src_path} equal to comitted"
82
+ return true
83
+ end
84
+
85
+ true
86
+ end
87
+
88
+ def update_working_copy(file, new_hash)
89
+ return unless update
90
+
91
+ begin
92
+ patch_working_file file, new_hash
93
+ rescue StandardError => e
94
+ warning "failed updating #{file.src_path} in working copy: #{e}"
95
+ end
96
+ end
97
+
98
+ def format_object(file)
99
+ info "Formatting #{file.src_path}"
100
+
101
+ format_command = formatter.sub('{}', file.src_path.shellescape)
102
+
103
+ pid1, r = pipe_command 'git', 'cat-file', '-p', file.dst_hash
104
+ pid2, r = pipe_command format_command, source: r
105
+
106
+ write_args = write ? ['-w'] : []
107
+ pid3, r = pipe_command 'git', 'hash-object', *write_args, '--stdin', source: r
108
+
109
+ result = read_output(r, lines: false).chomp
110
+
111
+ Process.wait pid1
112
+ raise "Cannot read #{file.dst_hash} from object database" unless $CHILD_STATUS.success?
113
+
114
+ Process.wait pid2
115
+ raise "Error formatting #{file.src_path}" unless $CHILD_STATUS.success?
116
+
117
+ Process.wait pid3
118
+ raise 'Error writing formatted file back to object database' unless $CHILD_STATUS.success? && !result.empty?
119
+
120
+ result
121
+ end
122
+
123
+ def object_is_empty(hash)
124
+ size = get_output('git', 'cat-file', '-s', hash).first.to_i
125
+ size.zero?
126
+ end
127
+
128
+ def patch_working_file(file, new_hash)
129
+ info 'Updating working copy'
130
+
131
+ patch = get_output 'git', 'diff', file.dst_hash, new_hash, lines: false, silent: true
132
+ patch.gsub! "a/#{file.dst_hash}", "a/#{file.src_path}"
133
+ patch.gsub! "b/#{new_hash}", "b/#{file.src_path}"
134
+
135
+ input, patch_out = IO.pipe
136
+ pid, r = pipe_command 'git', 'apply', '-', source: input
137
+
138
+ patch_out.write patch
139
+ patch_out.close
140
+
141
+ read_output r
142
+
143
+ Process.wait pid
144
+ raise 'Error applying patch' unless $CHILD_STATUS.success?
145
+ end
146
+
147
+ def replace_file_in_index(file, new_hash)
148
+ get_output 'git', 'update-index', '--cacheinfo', "#{file.dst_mode},#{new_hash},#{file.src_path}"
149
+ end
150
+
151
+ def validate_patterns(patterns)
152
+ patterns.each do |pattern|
153
+ fail! "Negative pattern '#{pattern}' is not yet supported" if pattern.start_with? '!'
154
+ end
155
+ end
156
+ end
157
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class FormatStaged
4
- VERSION = '0.0.2'
3
+ module FormatStaged
4
+ VERSION = '0.1.1'
5
5
  end
data/lib/format_staged.rb CHANGED
@@ -1,112 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'English'
4
- require 'format-staged/version'
5
- require 'format-staged/entry'
6
- require 'format-staged/io'
7
- require 'shellwords'
3
+ require_relative 'format-staged/job'
8
4
 
9
- class FormatStaged
10
- attr_reader :formatter, :patterns, :update, :write, :verbose
11
-
12
- def initialize(formatter:, patterns:, update: true, write: true, verbose: true)
13
- @formatter = formatter
14
- @patterns = patterns
15
- @update = update
16
- @write = write
17
- @verbose = verbose
18
- end
19
-
20
- def run
21
- root = get_output('git', 'rev-parse', '--show-toplevel').first
22
-
23
- files = get_output('git', 'diff-index', '--cached', '--diff-filter=AM', '--no-renames', 'HEAD')
24
- .map { |line| Entry.new(line, root: root) }
25
- .reject(&:symlink?)
26
- .filter { |entry| entry.matches?(@patterns) }
27
-
28
- files.each do |file|
29
- format_file(file)
30
- end
31
- end
32
-
33
- def format_file(file)
34
- new_hash = format_object file
35
-
36
- return true unless write
37
-
38
- if new_hash == file.dst_hash
39
- puts "Unchanged #{file.src_path}"
40
- return false
41
- end
42
-
43
- if object_is_empty new_hash
44
- puts "Skipping #{file.src_path}, formatted file is empty"
45
- return false
46
- end
47
-
48
- replace_file_in_index file, new_hash
49
-
50
- if update
51
- begin
52
- patch_working_file file, new_hash
53
- rescue StandardError => e
54
- puts "Warning: failed updating #{file.src_path} in working copy: #{e}"
55
- end
56
- end
57
-
58
- true
59
- end
60
-
61
- def format_object(file)
62
- puts "Formatting #{file.src_path}"
63
-
64
- format_command = formatter.sub('{}', file.src_path.shellescape)
65
-
66
- pid1, r = pipe_command 'git', 'cat-file', '-p', file.dst_hash
67
- pid2, r = pipe_command format_command, source: r
68
- pid3, r = pipe_command 'git', 'hash-object', '-w', '--stdin', source: r
69
-
70
- result = r.readlines.map(&:chomp)
71
- if @verbose
72
- result.each do |line|
73
- puts "< #{line}"
74
- end
75
- end
76
-
77
- Process.wait pid1
78
- raise "Cannot read #{file.dst_hash} from object database" unless $CHILD_STATUS.success?
79
-
80
- Process.wait pid2
81
- raise "Error formatting #{file.src_path}" unless $CHILD_STATUS.success?
82
-
83
- Process.wait pid3
84
- raise 'Error writing formatted file back to object database' unless $CHILD_STATUS.success? && !result.empty?
85
-
86
- result.first
87
- end
88
-
89
- def object_is_empty(hash)
90
- size = get_output('git', 'cat-file', '-s', hash).first.to_i
91
- size.zero?
92
- end
93
-
94
- def patch_working_file(file, new_hash)
95
- patch = get_output 'git', 'diff', file.dst_hash, new_hash, lines: false
96
- patch.gsub! "a/#{file.dst_hash}", "a/#{file.src_path}"
97
- patch.gsub! "b/#{new_hash}", "b/#{file.src_path}"
98
-
99
- input, patch_out = IO.pipe
100
- pid, r = pipe_command 'git', 'apply', '-', source: input
101
-
102
- patch_out.write patch
103
- patch_out.close
104
-
105
- Process.wait pid
106
- raise 'Error applying patch' unless $CHILD_STATUS.success?
107
- end
108
-
109
- def replace_file_in_index(file, new_hash)
110
- get_output 'git', 'update-index', '--cacheinfo', "#{file.dst_mode},#{new_hash},#{file.src_path}"
5
+ ##
6
+ # FormatStaged module
7
+ module FormatStaged
8
+ def self.run(**options)
9
+ Job.new(**options).run
111
10
  end
112
11
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: format-staged
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sven Weidauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-25 00:00:00.000000000 Z
11
+ date: 2022-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: rake
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -24,6 +38,20 @@ dependencies:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
40
  version: '13.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: rubocop
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +80,20 @@ dependencies:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
82
  version: '0.6'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
55
97
  description: git format staged
56
98
  email: sven@5sw.de
57
99
  executables:
@@ -62,6 +104,7 @@ files:
62
104
  - bin/git-format-staged
63
105
  - lib/format-staged/entry.rb
64
106
  - lib/format-staged/io.rb
107
+ - lib/format-staged/job.rb
65
108
  - lib/format-staged/version.rb
66
109
  - lib/format_staged.rb
67
110
  homepage: https://github.com/5sw/format-staged
@@ -75,7 +118,7 @@ require_paths:
75
118
  - lib
76
119
  required_ruby_version: !ruby/object:Gem::Requirement
77
120
  requirements:
78
- - - "~>"
121
+ - - ">="
79
122
  - !ruby/object:Gem::Version
80
123
  version: '2.7'
81
124
  required_rubygems_version: !ruby/object:Gem::Requirement