flatito 0.1.4 → 0.1.5
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 +4 -4
- data/README.md +27 -0
- data/exe/flatito +24 -1
- data/lib/flatito/diff_parser.rb +163 -0
- data/lib/flatito/diff_source.rb +70 -0
- data/lib/flatito/flatten_yaml.rb +1 -1
- data/lib/flatito/renderer.rb +10 -1
- data/lib/flatito/version.rb +1 -1
- data/lib/flatito.rb +61 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13b9642a9265396749175e81e627ef6a34371cd69ae3d4016a04810673a51049
|
|
4
|
+
data.tar.gz: c703addcb73ab4785c859db957c73cbcc25c6362df63bf8d44914118f81e73ae
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1545949a89946d2676e613b4cd90cf9b30adf285671c2749dd852dd0f4db76bf2671dc32f2014bab15a89276e483ca6b1c0660e9f99866d26eebcc8e1f86bf4a
|
|
7
|
+
data.tar.gz: 035f6c7d3089928ddad489370aeac0da9f53bdf7b2c86114516c849b5f196dc5521a059a9762c000c26a02bbf5b045c8df53e3e769fdbc3adccfe38722b30998
|
data/README.md
CHANGED
|
@@ -32,6 +32,7 @@ Usage: flatito PATH [options]
|
|
|
32
32
|
Example: flatito . -k "search string" -e "json,yaml"
|
|
33
33
|
Example: flatito . -c "search value"
|
|
34
34
|
Example: cat file.yaml | flatito -k "search string"
|
|
35
|
+
Example: git diff | flatito -k "search string"
|
|
35
36
|
|
|
36
37
|
-h, --help Prints this help
|
|
37
38
|
-V, --version Show version
|
|
@@ -42,6 +43,7 @@ Example: cat file.yaml | flatito -k "search string"
|
|
|
42
43
|
-e, --extensions=EXTENSIONS File extensions to search, separated by comma, default: (json,yaml,yaml)
|
|
43
44
|
--no-skipping Do not skip hidden files
|
|
44
45
|
--no-gitignore Do not respect .gitignore
|
|
46
|
+
--side=SIDE When input is a diff: before, after, or both (default: both)
|
|
45
47
|
```
|
|
46
48
|
|
|
47
49
|
Searches are case-insensitive by default. Use `-s` to force exact case matching.
|
|
@@ -53,6 +55,31 @@ Both `-k` and `-c` support regular expressions and can be combined:
|
|
|
53
55
|
flatito . -k "database" -c "production"
|
|
54
56
|
```
|
|
55
57
|
|
|
58
|
+
### Searching inside a `git diff`
|
|
59
|
+
|
|
60
|
+
When the input piped through stdin is a unified diff (e.g. the output of `git diff`),
|
|
61
|
+
flatito reports only the YAML/JSON entries whose lines were added (`+`) or
|
|
62
|
+
removed (`-`) by the diff. Each result is prefixed accordingly.
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
# Show every changed key in the working tree
|
|
66
|
+
git diff | flatito
|
|
67
|
+
|
|
68
|
+
# Inspect a specific commit, branch range, or staged changes
|
|
69
|
+
git diff HEAD~1 | flatito -k "version"
|
|
70
|
+
git diff main..feature | flatito -c "production"
|
|
71
|
+
git diff --cached | flatito
|
|
72
|
+
|
|
73
|
+
# Pick a side: only what was added, only what was removed, or both
|
|
74
|
+
git diff | flatito --side=after
|
|
75
|
+
git diff | flatito --side=before
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
When the diff includes git index hashes (the default for `git diff`), flatito
|
|
79
|
+
reads the original content via `git cat-file` so nested keys retain their
|
|
80
|
+
parent context. If those blobs are unavailable, it falls back to reconstructing
|
|
81
|
+
the changed regions from the diff hunks alone.
|
|
82
|
+
|
|
56
83
|
## Development
|
|
57
84
|
|
|
58
85
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/exe/flatito
CHANGED
|
@@ -6,6 +6,7 @@ require "optparse"
|
|
|
6
6
|
|
|
7
7
|
# If no arguments are given, print help
|
|
8
8
|
stdin = $stdin.read unless $stdin.tty?
|
|
9
|
+
stdin = nil if stdin && stdin.empty?
|
|
9
10
|
ARGV << "-h" if ARGV.empty? && !stdin
|
|
10
11
|
|
|
11
12
|
options = {}
|
|
@@ -15,6 +16,7 @@ OptionParser.new do |opts|
|
|
|
15
16
|
Example: flatito . -k "search string" -e "json,yaml"
|
|
16
17
|
Example: flatito . -c "search value"
|
|
17
18
|
Example: cat file.yaml | flatito -k "search string"
|
|
19
|
+
Example: git diff | flatito -k "search string"
|
|
18
20
|
HEREDOC
|
|
19
21
|
|
|
20
22
|
opts.on("-h", "--help", "Prints this help") do
|
|
@@ -54,12 +56,33 @@ OptionParser.new do |opts|
|
|
|
54
56
|
opts.on("--no-gitignore", "Do not respect .gitignore") do
|
|
55
57
|
options[:gitignore] = false
|
|
56
58
|
end
|
|
59
|
+
|
|
60
|
+
opts.on("--side=SIDE", %w[before after both],
|
|
61
|
+
"When input is a diff, which side to report (before, after, both). Default: both") do |s|
|
|
62
|
+
options[:side] = s.to_sym
|
|
63
|
+
end
|
|
57
64
|
end.parse!
|
|
58
65
|
|
|
59
66
|
Flatito::Config.prepare_with_options(options)
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
def diff_from_argv(argv)
|
|
69
|
+
return nil unless argv.size == 1
|
|
70
|
+
|
|
71
|
+
path = argv.first
|
|
72
|
+
return nil unless File.file?(path)
|
|
73
|
+
|
|
74
|
+
content = File.read(path)
|
|
75
|
+
Flatito::DiffParser.diff?(content) ? content : nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
argv_diff = diff_from_argv(ARGV)
|
|
79
|
+
|
|
80
|
+
matched = if stdin && Flatito::DiffParser.diff?(stdin)
|
|
81
|
+
Flatito.from_diff(stdin, options)
|
|
82
|
+
elsif stdin
|
|
62
83
|
Flatito.flat_content(stdin, options)
|
|
84
|
+
elsif argv_diff
|
|
85
|
+
Flatito.from_diff(argv_diff, options)
|
|
63
86
|
else
|
|
64
87
|
Flatito.search(ARGV, options)
|
|
65
88
|
end
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Flatito
|
|
6
|
+
class DiffParser
|
|
7
|
+
File = Struct.new(
|
|
8
|
+
:path, :before_path, :after_path,
|
|
9
|
+
:before_blob, :after_blob,
|
|
10
|
+
:hunks, :added_lines, :removed_lines,
|
|
11
|
+
:new_file, :deleted_file,
|
|
12
|
+
keyword_init: true
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Hunk = Struct.new(:old_start, :old_count, :new_start, :new_count, :lines, keyword_init: true)
|
|
16
|
+
|
|
17
|
+
HUNK_HEADER = /\A@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/
|
|
18
|
+
INDEX_LINE = /\Aindex ([0-9a-f]+)\.\.([0-9a-f]+)/
|
|
19
|
+
|
|
20
|
+
def self.parse(content)
|
|
21
|
+
new(content).parse
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.diff?(content)
|
|
25
|
+
return false if content.nil? || content.empty?
|
|
26
|
+
|
|
27
|
+
head = content.byteslice(0, 4096).to_s
|
|
28
|
+
head.start_with?("diff --git ") ||
|
|
29
|
+
(head.include?("\n--- ") && head.include?("\n+++ ") && head.include?("\n@@ "))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(content)
|
|
33
|
+
@lines = content.lines
|
|
34
|
+
@i = 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parse
|
|
38
|
+
files = []
|
|
39
|
+
current = nil
|
|
40
|
+
|
|
41
|
+
while @i < @lines.size
|
|
42
|
+
line = @lines[@i]
|
|
43
|
+
|
|
44
|
+
if line.start_with?("diff --git ")
|
|
45
|
+
files << current if current && !current.path.nil?
|
|
46
|
+
current = new_file_record
|
|
47
|
+
@i += 1
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
unless current
|
|
52
|
+
@i += 1
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if (m = line.match(INDEX_LINE))
|
|
57
|
+
current.before_blob = m[1]
|
|
58
|
+
current.after_blob = m[2]
|
|
59
|
+
elsif line.start_with?("new file mode")
|
|
60
|
+
current.new_file = true
|
|
61
|
+
elsif line.start_with?("deleted file mode")
|
|
62
|
+
current.deleted_file = true
|
|
63
|
+
elsif line.start_with?("--- ")
|
|
64
|
+
handle_minus_header(line, current)
|
|
65
|
+
elsif line.start_with?("+++ ")
|
|
66
|
+
handle_plus_header(line, current)
|
|
67
|
+
elsif (m = line.match(HUNK_HEADER))
|
|
68
|
+
@i += 1
|
|
69
|
+
consume_hunk(current, m)
|
|
70
|
+
next
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@i += 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
files << current if current && !current.path.nil?
|
|
77
|
+
files
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def new_file_record
|
|
83
|
+
File.new(
|
|
84
|
+
hunks: [],
|
|
85
|
+
added_lines: Set.new,
|
|
86
|
+
removed_lines: Set.new,
|
|
87
|
+
new_file: false,
|
|
88
|
+
deleted_file: false
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
DEV_NULL = "/dev/null" # rubocop:disable Style/FileNull
|
|
93
|
+
private_constant :DEV_NULL
|
|
94
|
+
|
|
95
|
+
def handle_minus_header(line, current)
|
|
96
|
+
raw = line[4..].to_s.chomp
|
|
97
|
+
path = raw.split("\t", 2).first.to_s
|
|
98
|
+
if path == DEV_NULL
|
|
99
|
+
current.new_file = true
|
|
100
|
+
elsif path.start_with?("a/")
|
|
101
|
+
current.before_path = path[2..]
|
|
102
|
+
else
|
|
103
|
+
current.before_path = path
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def handle_plus_header(line, current)
|
|
108
|
+
raw = line[4..].to_s.chomp
|
|
109
|
+
path = raw.split("\t", 2).first.to_s
|
|
110
|
+
if path == DEV_NULL
|
|
111
|
+
current.deleted_file = true
|
|
112
|
+
elsif path.start_with?("b/")
|
|
113
|
+
current.after_path = path[2..]
|
|
114
|
+
else
|
|
115
|
+
current.after_path = path
|
|
116
|
+
end
|
|
117
|
+
current.path ||= current.after_path || current.before_path
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def consume_hunk(current, header_match)
|
|
121
|
+
old_ln = header_match[1].to_i
|
|
122
|
+
new_ln = header_match[3].to_i
|
|
123
|
+
hunk = Hunk.new(
|
|
124
|
+
old_start: old_ln,
|
|
125
|
+
old_count: (header_match[2] || "1").to_i,
|
|
126
|
+
new_start: new_ln,
|
|
127
|
+
new_count: (header_match[4] || "1").to_i,
|
|
128
|
+
lines: []
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
while @i < @lines.size
|
|
132
|
+
hl = @lines[@i]
|
|
133
|
+
break if hl.start_with?("diff --git ", "@@ ", "--- ", "+++ ")
|
|
134
|
+
|
|
135
|
+
if hl.start_with?("+")
|
|
136
|
+
hunk.lines << { kind: :add, text: hl[1..].to_s }
|
|
137
|
+
current.added_lines << new_ln
|
|
138
|
+
new_ln += 1
|
|
139
|
+
elsif hl.start_with?("-")
|
|
140
|
+
hunk.lines << { kind: :del, text: hl[1..].to_s }
|
|
141
|
+
current.removed_lines << old_ln
|
|
142
|
+
old_ln += 1
|
|
143
|
+
elsif hl.start_with?(" ")
|
|
144
|
+
hunk.lines << { kind: :context, text: hl[1..].to_s }
|
|
145
|
+
old_ln += 1
|
|
146
|
+
new_ln += 1
|
|
147
|
+
elsif hl.start_with?("\\")
|
|
148
|
+
# "" — ignore
|
|
149
|
+
elsif hl == "\n" || hl.empty?
|
|
150
|
+
hunk.lines << { kind: :context, text: hl }
|
|
151
|
+
old_ln += 1
|
|
152
|
+
new_ln += 1
|
|
153
|
+
else
|
|
154
|
+
break
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
@i += 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
current.hunks << hunk
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "English"
|
|
4
|
+
|
|
5
|
+
module Flatito
|
|
6
|
+
module DiffSource
|
|
7
|
+
NULL_BLOB = "0000000"
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def contents_for(file)
|
|
12
|
+
before = file.new_file ? nil : load_side(file.before_blob, file.before_path, file)
|
|
13
|
+
after = file.deleted_file ? nil : load_side(file.after_blob, file.after_path, file, after_side: true)
|
|
14
|
+
[before, after]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def load_side(blob, path, file, after_side: false)
|
|
18
|
+
content = blob_content(blob)
|
|
19
|
+
return content if content
|
|
20
|
+
|
|
21
|
+
content = working_tree_content(path) if after_side
|
|
22
|
+
return content if content
|
|
23
|
+
|
|
24
|
+
reconstruct_from_hunks(file, after_side: after_side)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def blob_content(blob)
|
|
28
|
+
return nil if blob.nil? || blob.start_with?(NULL_BLOB)
|
|
29
|
+
|
|
30
|
+
output = IO.popen(["git", "cat-file", "-p", blob], err: File::NULL, &:read)
|
|
31
|
+
return nil unless $CHILD_STATUS&.exitstatus&.zero?
|
|
32
|
+
|
|
33
|
+
output
|
|
34
|
+
rescue Errno::ENOENT
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def working_tree_content(path)
|
|
39
|
+
return nil if path.nil? || path.empty?
|
|
40
|
+
return nil unless ::File.file?(path)
|
|
41
|
+
|
|
42
|
+
::File.read(path)
|
|
43
|
+
rescue StandardError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def reconstruct_from_hunks(file, after_side:)
|
|
48
|
+
lines = []
|
|
49
|
+
file.hunks.each do |hunk|
|
|
50
|
+
target_line = after_side ? hunk.new_start : hunk.old_start
|
|
51
|
+
pad_until(lines, target_line - 1)
|
|
52
|
+
hunk.lines.each do |entry|
|
|
53
|
+
case entry[:kind]
|
|
54
|
+
when :context
|
|
55
|
+
lines << entry[:text]
|
|
56
|
+
when :add
|
|
57
|
+
lines << entry[:text] if after_side
|
|
58
|
+
when :del
|
|
59
|
+
lines << entry[:text] unless after_side
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
lines.join
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def pad_until(lines, target_index)
|
|
67
|
+
lines << "\n" while lines.size < target_index
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/flatito/flatten_yaml.rb
CHANGED
data/lib/flatito/renderer.rb
CHANGED
|
@@ -55,7 +55,16 @@ module Flatito
|
|
|
55
55
|
""
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
marker = marker_prefix(item)
|
|
59
|
+
stdout.puts "#{marker}#{line_number} #{matched_string(item.key)} #{value}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def marker_prefix(item)
|
|
63
|
+
case item.marker
|
|
64
|
+
when :add then colorize("+ ", :green)
|
|
65
|
+
when :del then colorize("- ", :red)
|
|
66
|
+
else ""
|
|
67
|
+
end
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
private
|
data/lib/flatito/version.rb
CHANGED
data/lib/flatito.rb
CHANGED
|
@@ -12,8 +12,12 @@ require_relative "flatito/renderer"
|
|
|
12
12
|
require_relative "flatito/regex_from_search"
|
|
13
13
|
require_relative "flatito/print_items"
|
|
14
14
|
require_relative "flatito/config"
|
|
15
|
+
require_relative "flatito/diff_parser"
|
|
16
|
+
require_relative "flatito/diff_source"
|
|
15
17
|
|
|
16
18
|
module Flatito
|
|
19
|
+
DEFAULT_DIFF_EXTENSIONS = %w[.json .yml .yaml].freeze
|
|
20
|
+
|
|
17
21
|
class << self
|
|
18
22
|
def search(paths, options = {})
|
|
19
23
|
Finder.new(paths, options).call
|
|
@@ -26,5 +30,62 @@ module Flatito
|
|
|
26
30
|
items = FlattenYaml.items_from_content(content)
|
|
27
31
|
PrintItems.new(options[:search], options[:search_value], case_sensitive: options[:case_sensitive]).print(items) || false
|
|
28
32
|
end
|
|
33
|
+
|
|
34
|
+
def from_diff(diff_content, options = {})
|
|
35
|
+
side = (options[:side] || :both).to_sym
|
|
36
|
+
extensions = normalize_extensions(options[:extensions])
|
|
37
|
+
printer = PrintItems.new(options[:search], options[:search_value],
|
|
38
|
+
case_sensitive: options[:case_sensitive])
|
|
39
|
+
|
|
40
|
+
matched = false
|
|
41
|
+
DiffParser.parse(diff_content).each do |file|
|
|
42
|
+
next unless extension_match?(file.path, extensions)
|
|
43
|
+
|
|
44
|
+
before, after = DiffSource.contents_for(file)
|
|
45
|
+
items = collect_items(file, before, after, side)
|
|
46
|
+
next if items.empty?
|
|
47
|
+
|
|
48
|
+
matched = true if printer.print(items, file.path)
|
|
49
|
+
end
|
|
50
|
+
matched
|
|
51
|
+
rescue Interrupt
|
|
52
|
+
warn "\nInterrupted"
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def collect_items(file, before, after, side)
|
|
59
|
+
items = []
|
|
60
|
+
if %i[after both].include?(side) && after
|
|
61
|
+
items.concat(filter_changed(FlattenYaml.items_from_content(after, pathname: file.path),
|
|
62
|
+
file.added_lines, :add))
|
|
63
|
+
end
|
|
64
|
+
if %i[before both].include?(side) && before
|
|
65
|
+
items.concat(filter_changed(FlattenYaml.items_from_content(before, pathname: file.path),
|
|
66
|
+
file.removed_lines, :del))
|
|
67
|
+
end
|
|
68
|
+
items.sort_by { |item| [item.line, item.marker == :del ? 0 : 1] }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def filter_changed(items, line_set, marker)
|
|
72
|
+
items.filter_map do |item|
|
|
73
|
+
next unless line_set.include?(item.line)
|
|
74
|
+
|
|
75
|
+
FlattenYaml::Item.new(key: item.key, value: item.value, line: item.line, marker: marker)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def normalize_extensions(extensions)
|
|
80
|
+
list = extensions || %w[json yml yaml]
|
|
81
|
+
list.map { |ext| ext.start_with?(".") ? ext : ".#{ext}" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def extension_match?(path, extensions)
|
|
85
|
+
return false if path.nil?
|
|
86
|
+
|
|
87
|
+
ext = ::File.extname(path)
|
|
88
|
+
extensions.include?(ext)
|
|
89
|
+
end
|
|
29
90
|
end
|
|
30
91
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flatito
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- José Galisteo
|
|
@@ -42,6 +42,8 @@ files:
|
|
|
42
42
|
- exe/flatito
|
|
43
43
|
- lib/flatito.rb
|
|
44
44
|
- lib/flatito/config.rb
|
|
45
|
+
- lib/flatito/diff_parser.rb
|
|
46
|
+
- lib/flatito/diff_source.rb
|
|
45
47
|
- lib/flatito/finder.rb
|
|
46
48
|
- lib/flatito/flatten_yaml.rb
|
|
47
49
|
- lib/flatito/json_scanner.rb
|