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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d284d29a739750e07de8ce5593b8b01923aef64a5809cf3abdfd298d9af81a03
4
- data.tar.gz: 93a0319f3a2f1542e048de1acd9edf982a022a7e28028fa070a3d6f7973cc046
3
+ metadata.gz: 13b9642a9265396749175e81e627ef6a34371cd69ae3d4016a04810673a51049
4
+ data.tar.gz: c703addcb73ab4785c859db957c73cbcc25c6362df63bf8d44914118f81e73ae
5
5
  SHA512:
6
- metadata.gz: 8a8c13d42c11178c2792ea5e2ec860493c5a7e4573ffdd6cc5ea2d8a2619a54c99019368155a2958d6dbeb86b20dac815928214a956582fe5f19abd16b3a56d6
7
- data.tar.gz: 7bd6301af1fd634ea93abcac2a6272d94f8c1ad7409581e5b0cc42385845c31374084a96fce9785777c3f0b3765abbd195588e3c86a12771130bbb03b7ea89e3
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
- matched = if stdin
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Flatito
4
4
  class FlattenYaml
5
- Item = Struct.new(:key, :value, :line, keyword_init: true)
5
+ Item = Struct.new(:key, :value, :line, :marker, keyword_init: true)
6
6
  class << self
7
7
  def items_from_path(pathname)
8
8
  content = File.read(pathname)
@@ -55,7 +55,16 @@ module Flatito
55
55
  ""
56
56
  end
57
57
 
58
- stdout.puts "#{line_number} #{matched_string(item.key)} #{value}"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flatito
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
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
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