format-staged 0.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f8fd81e745a1b6710724e4834252786c1775928cb3ca8281b4acb94f87bf0946
4
+ data.tar.gz: 05f8dfa70f6405d390bf19e6c9c515ebed6647b714ecc83122e28b420f5934bf
5
+ SHA512:
6
+ metadata.gz: 6d04e7ef3312f160d22796fa6c0b9f923405b7c65540cd107641c2c668d312f66859d9c661b00a3e43fbddec56fc4b888d8823057bd64d652147d8aa90ff57bf
7
+ data.tar.gz: 61873bda257b0fd6d89abb4bb191f32694ea4dfb6e7f36b138d84f45b77091ba64b458195debdf498f3503b55800e3f685f170a8e0b3054b5895c324ebf8f4ec
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env ruby
2
+ require 'format-staged'
3
+ require 'optparse'
4
+
5
+ parameters = {
6
+ :update => true,
7
+ :write => true,
8
+ :verbose => false,
9
+ }
10
+
11
+ parser = OptionParser.new do |opt|
12
+ opt.banner = "Usage: #{opt.program_name} [options] [patterns]"
13
+ opt.separator ""
14
+ opt.on('-f', '--formatter COMMAND', 'Shell command to format files, will run once per file. Occurrences of the placeholder `{}` will be replaced with a path to the file being formatted. (Example: "prettier --stdin-filepath \'{}\'")') do |o|
15
+ parameters[:formatter] = o
16
+ end
17
+
18
+ opt.on('--[no-]update-working-tree', 'By default formatting changes made to staged file content will also be applied to working tree files via a patch. This option disables that behavior, leaving working tree files untouched.') do |value|
19
+ parameters[:update] = value
20
+ end
21
+
22
+ opt.on('--[no-]write', "Prevents #{opt.program_name} from modifying staged or working tree files. You can use this option to check staged changes with a linter instead of formatting. With this option stdout from the formatter command is ignored.") do |value|
23
+ parameters[:write] = value
24
+ end
25
+
26
+ opt.on("-v", "--[no-]verbose", 'Shows commands being run') do |value|
27
+ parameters[:verbose] = value
28
+ end
29
+
30
+ opt.separator ""
31
+
32
+ opt.on_tail('-h', '--help', 'Prints this help') do
33
+ puts opt
34
+ exit
35
+ end
36
+
37
+ opt.on_tail('--version', "Prints the version number and exits") do
38
+ puts FormatStaged::VERSION
39
+ exit
40
+ end
41
+ end
42
+
43
+ parser.parse!
44
+ parameters[:patterns] = ARGV
45
+
46
+ if !parameters[:formatter] or parameters[:patterns].empty?
47
+ puts "Missing formatter or file patterns!"
48
+
49
+ puts parser
50
+ exit
51
+ end
52
+
53
+ formatter = FormatStaged.new(**parameters)
54
+ formatter.run
@@ -0,0 +1,35 @@
1
+ class FormatStaged
2
+ class Entry
3
+ PATTERN = /^:(?<src_mode>\d+) (?<dst_mode>\d+) (?<src_hash>[a-f0-9]+) (?<dst_hash>[a-f0-9]+) (?<status>[A-Z])(?<score>\d+)?\t(?<src_path>[^\t]+)(?:\t(?<dst_path>[^\t]+))?$/
4
+
5
+ attr_reader :src_mode, :dst_mode, :src_hash, :dst_hash, :status, :score, :src_path, :dst_path, :path, :root
6
+
7
+ def initialize(line, root:)
8
+ matches = line.match(PATTERN) or raise "Cannot parse output #{line}"
9
+ @src_mode = matches[:src_mode]
10
+ @dst_mode = matches[:dst_mode]
11
+ @src_hash = matches[:src_hash]
12
+ @dst_hash = matches[:dst_hash]
13
+ @status = matches[:status]
14
+ @score = matches[:score]&.to_i
15
+ @src_path = matches[:src_path]
16
+ @dst_path = matches[:dst_path]
17
+ @path = File.expand_path(@src_path, root)
18
+ @root = root
19
+ end
20
+
21
+ def symlink?
22
+ @dst_mode == '120000'
23
+ end
24
+
25
+ def matches?(patterns)
26
+ result = false
27
+ patterns.each do |pattern|
28
+ if File.fnmatch? pattern, path, File::FNM_EXTGLOB
29
+ result = true
30
+ end
31
+ end
32
+ result
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,40 @@
1
+ class FormatStaged
2
+ def get_output(*args, lines: true)
3
+ puts '> ' + args.join(' ') if @verbose
4
+
5
+ output = IO.popen(args, err: :err) do |io|
6
+ if lines
7
+ io.readlines.map { |l| l.chomp }
8
+ else
9
+ io.read
10
+ end
11
+ end
12
+
13
+ if @verbose and lines
14
+ output.each do |line|
15
+ puts "< #{line}"
16
+ end
17
+ end
18
+
19
+ raise "Failed to run command" unless $?.success?
20
+
21
+ output
22
+ end
23
+
24
+ def pipe_command(*args, source: nil)
25
+ puts (source.nil? ? '> ' : '| ') + args.join(' ') if @verbose
26
+ r, w = IO.pipe
27
+
28
+ opts = {}
29
+ opts[:in] = source unless source.nil?
30
+ opts[:out] = w
31
+ opts[:err] = :err
32
+
33
+ pid = spawn(*args, **opts)
34
+
35
+ w.close
36
+ source &.close
37
+
38
+ return [pid, r]
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ class FormatStaged
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,109 @@
1
+ require 'format-staged/version'
2
+ require 'format-staged/entry'
3
+ require 'format-staged/io'
4
+ require 'shellwords'
5
+
6
+ class FormatStaged
7
+ attr_reader :formatter, :patterns, :update, :write, :verbose
8
+
9
+ def initialize(formatter:, patterns:, update: true, write: true, verbose: true)
10
+ @formatter = formatter
11
+ @patterns = patterns
12
+ @update = update
13
+ @write = write
14
+ @verbose = verbose
15
+ end
16
+
17
+ def run()
18
+ root = get_output('git', 'rev-parse', '--show-toplevel').first
19
+
20
+ files = get_output('git', 'diff-index', '--cached', '--diff-filter=AM', '--no-renames', 'HEAD')
21
+ .map { |line| Entry.new(line, root: root) }
22
+ .reject { |entry| entry.symlink? }
23
+ .filter { |entry| entry.matches?(@patterns) }
24
+
25
+ files.each do |file|
26
+ format_file(file)
27
+ end
28
+ end
29
+
30
+ def format_file(file)
31
+ new_hash = format_object file
32
+
33
+ return true if not write
34
+
35
+ if new_hash == file.dst_hash
36
+ puts "Unchanged #{file.src_path}"
37
+ return false
38
+ end
39
+
40
+ if object_is_empty new_hash
41
+ puts "Skipping #{file.src_path}, formatted file is empty"
42
+ return false
43
+ end
44
+
45
+ replace_file_in_index file, new_hash
46
+
47
+ if update
48
+ begin
49
+ patch_working_file file, new_hash
50
+ rescue => error
51
+ puts "Warning: failed updating #{file.src_path} in working copy: #{error}"
52
+ end
53
+ end
54
+
55
+ true
56
+ end
57
+
58
+ def format_object(file)
59
+ puts "Formatting #{file.src_path}"
60
+
61
+ format_command = formatter.sub("{}", file.src_path.shellescape)
62
+
63
+ pid1, r = pipe_command "git", "cat-file", "-p", file.dst_hash
64
+ pid2, r = pipe_command format_command, source: r
65
+ pid3, r = pipe_command "git", "hash-object", "-w", "--stdin", source: r
66
+
67
+ result = r.readlines.map { |it| it.chomp }
68
+ if @verbose
69
+ result.each do |line|
70
+ puts "< #{line}"
71
+ end
72
+ end
73
+
74
+ Process.wait pid1
75
+ raise "Cannot read #{file.dst_hash} from object database" unless $?.success?
76
+
77
+ Process.wait pid2
78
+ raise "Error formatting #{file.src_path}" unless $?.success?
79
+
80
+ Process.wait pid3
81
+ raise "Error writing formatted file back to object database" unless $?.success? && !result.empty?
82
+
83
+ result.first
84
+ end
85
+
86
+ def object_is_empty(hash)
87
+ size = get_output("git", "cat-file", "-s", hash).first.to_i
88
+ size == 0
89
+ end
90
+
91
+ def patch_working_file(file, new_hash)
92
+ patch = get_output "git", "diff", file.dst_hash, new_hash, lines: false
93
+ patch.gsub! "a/#{file.dst_hash}", "a/#{file.src_path}"
94
+ patch.gsub! "b/#{new_hash}", "b/#{file.src_path}"
95
+
96
+ input, patch_out = IO.pipe
97
+ pid, r = pipe_command "git", "apply", "-", source: input
98
+
99
+ patch_out.write patch
100
+ patch_out.close
101
+
102
+ Process.wait pid
103
+ raise "Error applying patch" unless $?.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}"
108
+ end
109
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: format-staged
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Sven Weidauer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-05-22 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: git format staged
14
+ email: sven@5sw.de
15
+ executables:
16
+ - git-format-staged
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - bin/git-format-staged
21
+ - lib/format-staged.rb
22
+ - lib/format-staged/entry.rb
23
+ - lib/format-staged/io.rb
24
+ - lib/format-staged/version.rb
25
+ homepage: https://github.com/5sw/format-staged
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 3.1.6
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: git format staged!
48
+ test_files: []