format-staged 0.0.3 → 0.1.0

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: 1692442034aaff731e62e22677ccd3026d4e8e644c88fdcc4ec2df9db06521b7
4
- data.tar.gz: a2f8d26b365c7c2b1597055490010398900335f0abc6e1dde9b6235adb6c9642
3
+ metadata.gz: f07500eeef0c1c28b74bbd28b5fd935b733d8c5b444e714db45b18ca450fdfce
4
+ data.tar.gz: 1cac2a6e5e80f10a0fd0df2c0349e49b7411f632709193902ba1eb9c3b646d8a
5
5
  SHA512:
6
- metadata.gz: e3bb54425caec06ca5ae575a9eb99726b57bd4974972a3db9a645f28cb199eb1e04f527305e4f2b6d1e0bd4abae2e7a5275041c2aa1c2b5baf37b9bfcec2bc44
7
- data.tar.gz: e118755923b6a019cccc20e1354721692fd38fc6c1f6dc09013c47ab9a5d9959277f365362696111c7887faa74526477b8d140b654812a0152840aa9a91d6e34
6
+ metadata.gz: ba546faa5200d31c2f7c4faa413196d31c6b45314fecd583a2905b1f0c52b7135a4e0f847384a6c5fb27b41e8a66e6813a2329b1cb65af1659a6c9250fa40daf
7
+ data.tar.gz: 82dd60bd9f48843a67a72c65bb4d455ea818502b6e6f34bc6c66b94a2aa6497bbc9981909ed1d266519d544cf48a95dd1ada6c8d73531eab07f49522c7083f3a
@@ -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,45 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'English'
4
- class FormatStaged
5
- def get_output(*args, lines: true, silent: false)
6
- puts "> #{args.join(' ')}" if @verbose
7
4
 
8
- r = IO.popen(args, err: :err)
9
- output = read_output(r, lines: lines, silent: silent)
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
10
11
 
11
- raise 'Failed to run command' unless $CHILD_STATUS.success?
12
+ r = IO.popen(args, err: :err)
13
+ output = read_output(r, lines: lines, silent: silent)
12
14
 
13
- output
14
- end
15
+ raise 'Failed to run command' unless $CHILD_STATUS.success?
15
16
 
16
- def pipe_command(*args, source: nil)
17
- puts (source.nil? ? '> ' : '| ') + args.join(' ') if @verbose
18
- r, w = IO.pipe
17
+ output
18
+ end
19
19
 
20
- opts = {}
21
- opts[:in] = source unless source.nil?
22
- opts[:out] = w
23
- opts[:err] = :err
20
+ def get_status(*args)
21
+ puts "> #{args.join(' ')}" if @verbose
22
+ result = system(*args)
24
23
 
25
- pid = spawn(*args, **opts)
24
+ raise 'Failed to run command' if result.nil?
26
25
 
27
- w.close
28
- source&.close
26
+ puts "? #{$CHILD_STATUS.exitstatus}" if @verbose
29
27
 
30
- [pid, r]
31
- end
28
+ $CHILD_STATUS.exitstatus
29
+ end
30
+
31
+ def pipe_command(*args, source: nil)
32
+ puts (source.nil? ? '> ' : '| ') + args.join(' ') if @verbose
33
+ r, w = IO.pipe
34
+
35
+ opts = {}
36
+ opts[:in] = source unless source.nil?
37
+ opts[:out] = w
38
+ opts[:err] = :err
39
+
40
+ pid = spawn(*args, **opts)
41
+
42
+ w.close
43
+ source&.close
44
+
45
+ [pid, r]
46
+ end
32
47
 
33
- def read_output(r, lines: true, silent: false)
34
- result = r.read
35
- splits = result.split("\n")
36
- if @verbose && !silent
37
- splits.each do |line|
38
- puts "< #{line}"
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
39
55
  end
56
+ output.close
57
+
58
+ lines ? splits : result
40
59
  end
41
- r.close
42
60
 
43
- lines ? splits : result
61
+ def fail!(message)
62
+ abort "💣 #{message.red}"
63
+ end
64
+
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
44
76
  end
45
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.3'
3
+ module FormatStaged
4
+ VERSION = '0.1.0'
5
5
  end
data/lib/format_staged.rb CHANGED
@@ -1,109 +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 = read_output(r, lines: false).chomp
71
-
72
- Process.wait pid1
73
- raise "Cannot read #{file.dst_hash} from object database" unless $CHILD_STATUS.success?
74
-
75
- Process.wait pid2
76
- raise "Error formatting #{file.src_path}" unless $CHILD_STATUS.success?
77
-
78
- Process.wait pid3
79
- raise 'Error writing formatted file back to object database' unless $CHILD_STATUS.success? && !result.empty?
80
-
81
- result
82
- end
83
-
84
- def object_is_empty(hash)
85
- size = get_output('git', 'cat-file', '-s', hash).first.to_i
86
- size.zero?
87
- end
88
-
89
- def patch_working_file(file, new_hash)
90
- patch = get_output 'git', 'diff', file.dst_hash, new_hash, lines: false, silent: true
91
- patch.gsub! "a/#{file.dst_hash}", "a/#{file.src_path}"
92
- patch.gsub! "b/#{new_hash}", "b/#{file.src_path}"
93
-
94
- input, patch_out = IO.pipe
95
- pid, r = pipe_command 'git', 'apply', '-', source: input
96
-
97
- patch_out.write patch
98
- patch_out.close
99
-
100
- read_output r
101
-
102
- Process.wait pid
103
- raise 'Error applying patch' unless $CHILD_STATUS.success?
104
- end
105
-
106
- def replace_file_in_index(file, new_hash)
107
- 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
108
10
  end
109
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.3
4
+ version: 0.1.0
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-06 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