minitest-holdify 1.2.0 → 1.3.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: 056a8cc8b2a69276cf8adc189965b484cb9742b6c4e71efaca3b194b56bc1965
4
- data.tar.gz: 1570f826edd18de9ee3dbbe6fa39d759f597ab5f5f4ebf452b015ae9cc26f240
3
+ metadata.gz: aff615590c52a4b538a92ae7e90dd24a6d4f5461e70b3cc240f5a30a819a8855
4
+ data.tar.gz: 6a0977317a054b2e6c89c403715c2815cd3d24d479161f83c622acb55ded2aeb
5
5
  SHA512:
6
- metadata.gz: c6caaf67f67c9992df20d00ab2fc498365ae09f13225e7469aa370b68f0a42cbb77c97c7e68825c21b42600a606ef4c468bcec011c54e2380945bb8f9394741f
7
- data.tar.gz: 30bf7d38f0df80f1a2b581c12ee7fbd1ca6811369fe4a2977e286d1876d37daabb1eb5cd8b8d9b81ceb40103dcf430003ef1ed8240db79ce5c5579680395ca7b
6
+ metadata.gz: 8f0553c5e76cced33e96d49adf7b89929d2d119d3f375ac36ea1aa3fac2a5786cfc65000aaefae5bf5066f94921840549927761a36b2f9832f6ea770a8a71702
7
+ data.tar.gz: 73ff0933e454a35def50e40b2cf46b82a45a4fa801c6dbb2dcb9a36d4ef11ddd91ee51649260f60e544ef581e7768942076aa1a4120aae4aeabf5a4d4a65cb79
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+ require 'tempfile'
5
+ require 'open3'
6
+
7
+ module Holdify
8
+ # Feedback report on failure
9
+ class Feedback
10
+ attr_reader :xxhi_ref
11
+
12
+ def initialize(hold, hold_ref, *args)
13
+ test_lno = hold.test_loc.lineno
14
+ index = hold.session[test_lno].size - 1 # current index
15
+ xxh = hold.store.xxh(test_lno)
16
+ yaml_path = hold.store.path
17
+
18
+ @xxhi_ref = "<<< @xxh[i] --> #{xxh}[#{index}]"
19
+
20
+ @yaml_lno = find_yaml_lno(yaml_path, test_lno, xxh, index)
21
+ @yaml_ref = Holdify.relative("#{yaml_path}:#{@yaml_lno}")
22
+
23
+ @hold_ref = Holdify.relative(hold_ref)
24
+ test_ref = Holdify.relative(hold.test_loc.to_s.sub(/:in .*$/, ''))
25
+ @test_ref = test_ref unless @hold_ref == test_ref
26
+
27
+ @expected, @actual, @message = *args
28
+
29
+ # Extend with features
30
+ extend(Color) if Holdify.color
31
+ return unless Holdify.git
32
+
33
+ extend GitDiff
34
+ extend(Color::GitDiff) if Holdify.color
35
+ end
36
+
37
+ def message = [@message, xxhi_ref, *file_refs, *diff, ''].join("\n")
38
+
39
+ def file_refs
40
+ ["--- @stored --> #{@yaml_ref}", "+++ @tested --> #{@hold_ref}"].tap do |refs|
41
+ refs << " --> #{@test_ref}" if @test_ref
42
+ end
43
+ end
44
+
45
+ def diff = ["- #{@expected.inspect}", "+ #{@actual.inspect}"]
46
+
47
+ private
48
+
49
+ def find_yaml_lno(yaml_path, test_lno, xxh, index)
50
+ found = false
51
+ count = -1
52
+ File.foreach(yaml_path).with_index(1) do |line, ln|
53
+ if found
54
+ next unless line.start_with?('-')
55
+
56
+ count += 1
57
+ next unless count == index
58
+
59
+ return ln
60
+ else
61
+ found = line.match(/^L#{test_lno} #{xxh}:$/)
62
+ end
63
+ end
64
+ end
65
+
66
+ # Methods enabling the git-diff feedback (no-color)
67
+ module GitDiff
68
+ def git_command = "git diff --no-index --no-color --unified=1000 #{@exp_path} #{@act_path}"
69
+
70
+ def diff
71
+ @exp_path = create_tempfile(@expected, 'expected').path
72
+ @act_path = create_tempfile(@actual, 'actual').path
73
+
74
+ stdout, = Open3.capture3(git_command)
75
+ regex = /\A[^@]*\r?\n/m # cleanup git headers
76
+ lines = stdout.sub(regex, '').split(/\r?\n/)
77
+
78
+ process_lines(lines)
79
+ ensure
80
+ # :nocov:
81
+ File.unlink(@exp_path) if @exp_path
82
+ File.unlink(@act_path) if @act_path
83
+ # :nocov:
84
+ end
85
+
86
+ def create_tempfile(obj, type)
87
+ Tempfile.create.tap do |file|
88
+ file.write(Store.hold_dump(obj).sub(/(?=\n)/, type))
89
+ file.close
90
+ end
91
+ end
92
+
93
+ def process_lines(lines)
94
+ width = 0
95
+ lineno = [@yaml_lno - 1]
96
+ lines.map.with_index do |line, i|
97
+ if i.zero? # @@ ... @@
98
+ width, line = render_hunk(line)
99
+ next line
100
+ end
101
+ next if i == 1 || (i == 2 && !Holdify.color) # ---
102
+
103
+ render_line(line, lineno, width)
104
+ end.compact
105
+ end
106
+
107
+ def render_hunk(line)
108
+ width = 0
109
+ hunk = line.gsub(/([-+]\d+),(\d+)\s+([-+]\d+),(\d+)/) do
110
+ v1, = $2.to_i - 1 # rubocop:disable Style/PerlBackrefs
111
+ v2 = $4.to_i - 1 # rubocop:disable Style/PerlBackrefs
112
+ width = (@yaml_lno + [v1, v2].max).to_s.length
113
+ "#{$1},#{v1} #{$3},#{v2}" # rubocop:disable Style/PerlBackrefs
114
+ end
115
+ [width, hunk]
116
+ end
117
+
118
+ def render_line(line, lineno, width)
119
+ type = line[0]
120
+ line = line[1..]
121
+ gutter = if type == '+'
122
+ ' ' * width
123
+ else
124
+ lineno[0] += 1
125
+ lineno[0].to_s.rjust(width)
126
+ end
127
+
128
+ "#{type}#{gutter} #{line}"
129
+ end
130
+ end
131
+
132
+ # Methods enabling ANSI feedback
133
+ module Color
134
+ SGR = { clear: "\e[0m",
135
+ red: "\e[31m",
136
+ green: "\e[32m",
137
+ yellow: "\e[33m",
138
+ magenta: "\e[35m" }.freeze
139
+
140
+ def wrap(color, string) = "#{SGR[color]}#{string}#{SGR[:clear]}"
141
+
142
+ def file_refs
143
+ refs = super
144
+ [wrap(:red, refs.shift), *refs.map { wrap(:green, _1) }]
145
+ end
146
+
147
+ def diff = [wrap(:red, "- #{@expected.inspect}"), wrap(:green, "+ #{@actual.inspect}")]
148
+
149
+ def xxhi_ref = wrap(:magenta, @xxhi_ref)
150
+
151
+ # Methods enabling the git-diff ANSI feedback
152
+ module GitDiff
153
+ def git_command = "git diff --no-index --color-words='[^ ]+|[ ]+' --unified=1000 #{@exp_path} #{@act_path}"
154
+
155
+ def render_line(line, lineno, width)
156
+ clean = line.gsub(/\e\[(1|22|0)m/, '').lstrip
157
+ added = clean.start_with?(SGR[:green]) && !line.include?(SGR[:red])
158
+ removed = clean.start_with?(SGR[:red]) && !line.include?(SGR[:green])
159
+ changed = line.include?(SGR[:red]) || line.include?(SGR[:green])
160
+
161
+ type, color = (added && ['+', :green]) || (removed && ['-', :red]) || (changed && ['~', :yellow])
162
+
163
+ sgr = SGR[color].to_s if color
164
+ gutter = if added
165
+ "#{sgr}#{type} #{' ' * width}#{SGR[:clear]}"
166
+ else
167
+ lineno[0] += 1
168
+ "#{sgr}#{type || ' '}#{lineno[0].to_s.rjust(width)}#{SGR[:clear]}"
169
+ end
170
+
171
+ "#{gutter} #{line}"
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
data/lib/holdify/hold.rb CHANGED
@@ -1,57 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Holdify
4
- # The *_hold statement (Assertion/Expectation)
4
+ # The *_hold Assertion/Expectation
5
5
  class Hold
6
- attr_reader :forced, :store
6
+ attr_reader :forced, :store, :test_loc, :session
7
7
 
8
8
  def initialize(test)
9
9
  @test = test
10
10
  @path, = test.method(test.name).source_location
11
11
  @store = Holdify.stores(@path)
12
- @session = Hash.new { |h, k| h[k] = [] } # { line => [values] }
12
+ @session = Hash.new { |h, k| h[k] = [] } # { lineno => [values] }
13
13
  @forced = [] # [ lines ]
14
14
  @added = [] # [ lines ]
15
15
  end
16
16
 
17
17
  def call(actual, force: false)
18
- location = find_location
19
- line = location.lineno
20
- raise "Could not find holdify statement at line #{line}" unless @store.xxh(line)
18
+ @test_loc = find_test_loc
19
+ lineno = @test_loc.lineno
20
+ raise "Could not find holdify statement at lineno #{lineno}" unless @store.xxh(lineno)
21
21
 
22
- @forced << line if force
22
+ @forced << lineno if force
23
23
 
24
- values = @store.get(line)
25
- index = @session[line].size
24
+ values = @store.get_values(lineno)
25
+ index = @session[lineno].size
26
26
  value = if force || Holdify.reconcile
27
27
  actual
28
28
  elsif values && index < values.size
29
29
  values[index]
30
30
  else
31
- @added << line
31
+ @added << lineno
32
32
  actual
33
33
  end
34
34
 
35
- @session[line] << value
35
+ @session[lineno] << value
36
36
  value
37
37
  end
38
38
 
39
39
  def save
40
- @added.each { |line| warn "[holdify] Held new value for #{@path}:#{line}" } unless Holdify.quiet
41
- @session.each { |line, values| @store.set(line, values) }
40
+ @added.each { |lineno| warn "[holdify] Held new value for #{Holdify.relative(@path)}:#{lineno}" } unless Holdify.quiet
41
+ @session.each { |lineno, values| @store.set_values(lineno, values) }
42
42
  end
43
43
 
44
- # The index of the current test/value
45
- def current_index(line) = @session[line].size - 1
46
-
47
- # Find the location in the test that triggered the hold
48
- def find_location
44
+ # Find the triggering LOC inside the test block/method
45
+ def find_test_loc
49
46
  caller_locations(2).find do |location|
50
47
  next unless location.path == @path
51
48
 
52
- label = location.base_label
49
+ label = location.base_label
53
50
  label == @test.name || label == '<top (required)>' || label == '<main>' || label.start_with?('<class:', '<module:')
54
51
  end
55
52
  end
53
+
54
+ def feedback(*) = Feedback.new(self, *).message
56
55
  end
57
56
  end
@@ -7,7 +7,7 @@ module Holdify
7
7
  class Source
8
8
  def initialize(path) = (@line_xxh, @xxh_lines = parse(path))
9
9
 
10
- def xxh(line) = @line_xxh[line]
10
+ def xxh(lineno) = @line_xxh[lineno]
11
11
 
12
12
  def lines(xxh) = @xxh_lines[xxh]
13
13
 
@@ -17,13 +17,13 @@ module Holdify
17
17
  line_xxh = {}
18
18
  xxh_lines = Hash.new { |h, k| h[k] = [] }
19
19
 
20
- File.foreach(path).with_index(1) do |text, line|
20
+ File.foreach(path).with_index(1) do |text, lineno|
21
21
  content = text.strip
22
22
  next if content.empty?
23
23
 
24
- xxh = Digest::XXH3_64bits.hexdigest(content)
25
- line_xxh[line] = xxh
26
- xxh_lines[xxh] << line
24
+ xxh = Digest::XXH3_64bits.hexdigest(content)
25
+ line_xxh[lineno] = xxh
26
+ xxh_lines[xxh] << lineno
27
27
  end
28
28
 
29
29
  [line_xxh, xxh_lines]
data/lib/holdify/store.rb CHANGED
@@ -1,20 +1,72 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'yaml'
4
+ require 'fileutils'
3
5
  require 'forwardable'
4
- require_relative 'source'
5
- require_relative 'ledger'
6
6
 
7
7
  module Holdify
8
8
  # Interface to the Source (code) and the Ledger (data)
9
9
  class Store
10
+ class NoAliasVisitor < Psych::Visitors::YAMLTree # :nodoc:
11
+ def register(target, obj); end
12
+ end
13
+
14
+ def self.hold_dump(obj)
15
+ visitor = NoAliasVisitor.create
16
+ visitor << obj
17
+ visitor.tree.to_yaml
18
+ end
10
19
  extend Forwardable
11
20
 
21
+ def_delegator :@source, :xxh
22
+ attr_reader :path
23
+
12
24
  def initialize(path)
25
+ @path = "#{path}#{Holdify.store_ext}"
13
26
  @source = Source.new(path)
14
- @ledger = Ledger.new(path, @source)
27
+ FileUtils.rm_f(@path) if Holdify.reconcile
28
+ @data = File.exist?(@path) ? load_and_align : {}
15
29
  end
16
30
 
17
- def_delegator :@source, :xxh
18
- def_delegators :@ledger, :get, :set, :persist, :lookup
31
+ def get_values(lineno) = @data[lineno]
32
+
33
+ def set_values(lineno, values) = (@data[lineno] = values)
34
+
35
+ def persist
36
+ return FileUtils.rm_f(@path) if @data.empty?
37
+
38
+ output = {}
39
+ @data.keys.sort.each do |lineno|
40
+ xxh = @source.xxh(lineno)
41
+ next unless xxh
42
+
43
+ output["L#{lineno} #{xxh}"] = @data[lineno]
44
+ end
45
+
46
+ File.write(@path, self.class.hold_dump(output))
47
+ end
48
+
49
+ private
50
+
51
+ def load_and_align
52
+ {}.tap do |aligned|
53
+ data = YAML.unsafe_load_file(@path) || {}
54
+ data.group_by { |k, _| k.split.last }.each do |xxh, entries|
55
+ lines = @source.lines(xxh)
56
+ next if lines.empty?
57
+
58
+ # Position of the held lines compared to the source lines
59
+ stayed, moved = entries.map { |key, values| { line: key[/\d+/].to_i, values: } }
60
+ .partition { lines.include?(_1[:line]) }
61
+ moved.sort_by! { _1[:line] }
62
+
63
+ # Align lines
64
+ lines.each do |lineno|
65
+ match = stayed.find { _1[:line] == lineno } || moved.shift
66
+ aligned[lineno] = match[:values] if match
67
+ end
68
+ end
69
+ end
70
+ end
19
71
  end
20
72
  end
data/lib/holdify.rb CHANGED
@@ -1,19 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'holdify/store'
4
3
  require_relative 'holdify/hold'
4
+ require_relative 'holdify/feedback'
5
+ require_relative 'holdify/source'
6
+ require_relative 'holdify/store'
5
7
 
6
8
  # The container module
7
9
  module Holdify
8
- VERSION = '1.2.0'
9
- CONFIG = { ext: '.yaml' }.freeze
10
+ VERSION = '1.3.1'
10
11
 
11
12
  class << self
12
- attr_accessor :reconcile, :quiet
13
+ attr_accessor :reconcile, :quiet, :git, :pwd, :color, :rel_paths, :store_ext
14
+
15
+ def persist_all! = @stores&.each_value(&:persist)
13
16
 
14
- def config
15
- @config ||= { git: system('git --version', out: File::NULL, err: File::NULL),
16
- color: !ENV.key?('NO_COLOR') }
17
+ def relative(path)
18
+ return path unless rel_paths
19
+
20
+ path.sub(%r{^#{pwd}/}, '')
17
21
  end
18
22
 
19
23
  def stores(path = nil)
@@ -23,10 +27,6 @@ module Holdify
23
27
  @stores[path] ||= Store.new(path)
24
28
  end
25
29
  end
26
-
27
- def persist_all!
28
- @stores&.each_value(&:persist)
29
- end
30
30
  end
31
31
  @mutex = Mutex.new
32
32
  @stores = {}
@@ -1,27 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'holdify'
4
- require 'holdify/failure'
5
4
 
6
5
  # Implement the minitest plugin
7
6
  module Minitest
8
- # Register the after_run hook to persist all data
9
- def self.plugin_holdify_init(_options)
10
- Minitest.after_run { Holdify.persist_all! }
11
- end
12
-
13
7
  # Set the Holdify options
14
- def self.plugin_holdify_options(opts, _options)
15
- opts.on '--holdify-reconcile', 'Reconcile the held values with the new ones' do
16
- Holdify.reconcile = true
17
- Holdify.quiet = true
8
+ def self.plugin_holdify_options(opts, options)
9
+ opts.on '--holdify-reconcile', 'Reconcile the held values with the new ones (enables quiet)' do
10
+ options[:holdify_reconcile] = true
11
+ options[:holdify_quiet] = true
18
12
  end
19
-
20
13
  opts.on '--holdify-quiet', 'Skip the warning on storing a new value' do
21
- Holdify.quiet = true
14
+ options[:holdify_quiet] = true
15
+ end
16
+ opts.on '--holdify-no-git-diff', 'Disable git-diff' do
17
+ options[:holdify_no_git_diff] = true
18
+ end
19
+ opts.on '--holdify-no-color', 'Disable colored output' do
20
+ options[:holdify_no_color] = true
21
+ end
22
+ opts.on '--holdify-no-rel-paths', 'Disable relative paths in file references' do
23
+ options[:holdify_no_rel_paths] = true
24
+ end
25
+ opts.on '--holdify-store-ext EXT', 'The yaml store extension (default .yaml)' do |ext|
26
+ options[:holdify_store_ext] = ext
22
27
  end
23
28
  end
24
29
 
30
+ # Register the after_run hook to persist all data
31
+ def self.plugin_holdify_init(options)
32
+ Holdify.reconcile = options[:holdify_reconcile]
33
+ Holdify.quiet = options[:holdify_quiet]
34
+ Holdify.git = system('git --version', out: File::NULL, err: File::NULL) unless options[:holdify_no_git_diff]
35
+ Holdify.pwd = Holdify.git ? `git rev-parse --show-toplevel`.strip : Dir.pwd
36
+ Holdify.color = !ENV.key?('NO_COLOR') unless options[:holdify_no_color]
37
+ Holdify.rel_paths = true unless options[:holdify_no_rel_paths]
38
+ Holdify.store_ext = options[:holdify_store_ext] || '.yaml'
39
+
40
+ Minitest.after_run { Holdify.persist_all! }
41
+ end
42
+
25
43
  # Reopen the minitest class
26
44
  class Test
27
45
  # Ensure store is tidied and saved after the test runs
@@ -34,7 +52,7 @@ module Minitest
34
52
 
35
53
  path, = method(name).source_location
36
54
  msg = +%([holdify] the value has been stored: remove the "!" suffix to pass the test\n)
37
- msg << @hold.forced.uniq.map { |line| " #{path}:#{line}" }.join("\n")
55
+ msg << @hold.forced.uniq.map { |lineno| " #{path}:#{lineno}" }.join("\n")
38
56
 
39
57
  raise Minitest::Assertion, msg
40
58
  end
@@ -50,23 +68,17 @@ module Minitest
50
68
  expected = @hold.(actual, **)
51
69
 
52
70
  begin
53
- if actual.nil?
71
+ if inspect
72
+ message = ['INSPECT[?]', message].compact.join(' ')
73
+ flunk(message)
74
+ elsif actual.nil?
54
75
  assert_nil expected, message
55
76
  else
56
77
  send(assertion || :assert_equal, expected, actual, message)
57
78
  end
58
79
  rescue Minitest::Assertion => e
59
- location = @hold.find_location
60
- metadata = @hold.store.lookup(location.lineno, @hold.current_index(location.lineno))
61
- hold_msg = Holdify::Failure.new(expected, actual, e.message, metadata:, location:).message
62
-
63
- msg = message ? "#{message}\n#{hold_msg}" : hold_msg
64
- raise Minitest::Assertion, msg
65
- end
66
-
67
- if inspect
68
- location = @hold.find_location
69
- warn "[holdify] The value from #{location.path}:#{location.lineno} is:\n[holdify] => #{actual.inspect}"
80
+ feedback = @hold.feedback(e.location, expected, actual, message)
81
+ raise(Minitest::Assertion, feedback)
70
82
  end
71
83
 
72
84
  expected
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minitest-holdify
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Domizio Demichelis
@@ -47,9 +47,8 @@ extra_rdoc_files: []
47
47
  files:
48
48
  - LICENSE.txt
49
49
  - lib/holdify.rb
50
- - lib/holdify/failure.rb
50
+ - lib/holdify/feedback.rb
51
51
  - lib/holdify/hold.rb
52
- - lib/holdify/ledger.rb
53
52
  - lib/holdify/source.rb
54
53
  - lib/holdify/store.rb
55
54
  - lib/minitest-holdify.rb
@@ -1,110 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'yaml'
4
- require 'tempfile'
5
- require 'open3'
6
-
7
- module Holdify
8
- # Diff print failure report
9
- class Failure
10
- GUTTER_WIDTH = 3
11
-
12
- def initialize(expected, actual, message, location:, metadata:)
13
- @expected = expected
14
- @actual = actual
15
- @message = message
16
- @location = location
17
- @metadata = metadata
18
- @config = Holdify.config
19
- end
20
-
21
- def message
22
- output = add_headers
23
- @config[:git] ? output.push(*add_diff) : output.push(@message)
24
- output.join("\n")
25
- end
26
-
27
- def add_headers
28
- ["#{ansi[:red]}--- @yaml#{ansi[:reset]} >>> #{@metadata[:path]}:#{@metadata[:line]}",
29
- "#{ansi[:green]}+++ @test#{ansi[:reset]} >>> #{@location.path}:#{@location.lineno}",
30
- "#{ansi[:inverse]}<-- #{@metadata[:key]} #{ansi[:reset]}"]
31
- end
32
-
33
- def ansi
34
- @config[:color] ? { inverse: "\e[7m", reset: "\e[0m", red: "\e[31m", green: "\e[32m" } : {}
35
- end
36
-
37
- def git_command
38
- if @config[:color]
39
- regex = '[^[:space:]]+|[[:space:]]+'
40
- %W[git diff --no-index --color-words=#{regex} --unified=1000 #{@exp_file.path} #{@act_file.path}]
41
- else
42
- %W[git diff --no-index --no-color --unified=1000 #{@exp_file.path} #{@act_file.path}]
43
- end
44
- end
45
-
46
- def add_diff
47
- @exp_file = create_tempfile(@expected)
48
- @act_file = create_tempfile(@actual)
49
- @config[:color] ? color_process_lines : process_lines
50
- rescue Errno::ENOENT
51
- []
52
- ensure
53
- @exp_file&.close!
54
- @act_file&.close!
55
- end
56
-
57
- def diff_lines
58
- stdout, = Open3.capture3(*git_command)
59
- return [] if stdout.to_s.empty?
60
-
61
- # Match everything from the start up to the LAST @@...--- sequence
62
- # .* is greedy, so it skips all earlier hunks
63
- regex = /\A.*@@.*?\r?\n[[:blank:]]*---(\e\[[0-9;]*m)?\r?\n/m
64
- body = stdout.sub(regex, '')
65
-
66
- body.split(/\r?\n/)
67
- end
68
-
69
- def color_process_lines
70
- line_num = @metadata[:line]
71
- diff_lines.map do |line|
72
- clean = line.gsub(/\e\[(1|22|0)m/, '').lstrip
73
- added = clean.start_with?(ansi[:green]) && !line.include?(ansi[:red])
74
-
75
- if added
76
- gutter = ' ' * GUTTER_WIDTH
77
- else
78
- gutter = line_num.to_s.rjust(GUTTER_WIDTH)
79
- line_num += 1
80
- end
81
-
82
- "#{ansi[:inverse]}#{gutter}#{ansi[:reset]} #{line}"
83
- end
84
- end
85
-
86
- def process_lines
87
- line_num = @metadata[:line]
88
- diff_lines.map do |line|
89
- type = line[0]
90
- line = line[1..]
91
-
92
- if type == '+'
93
- gutter = ' ' * GUTTER_WIDTH
94
- else
95
- gutter = line_num.to_s.rjust(GUTTER_WIDTH)
96
- line_num += 1
97
- end
98
-
99
- "#{gutter} #{type} #{line}"
100
- end
101
- end
102
-
103
- def create_tempfile(obj)
104
- Tempfile.new(%w[holdify .yaml]).tap do |file|
105
- file.write(YAML.dump(obj))
106
- file.flush
107
- end
108
- end
109
- end
110
- end
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'yaml'
4
- require 'fileutils'
5
-
6
- module Holdify
7
- # Manages the persistence and alignment of stored values
8
- class Ledger
9
- def initialize(path, source)
10
- @path = "#{path}#{CONFIG[:ext]}"
11
- @source = source
12
- FileUtils.rm_f(@path) if Holdify.reconcile
13
- @data = File.exist?(@path) ? load_and_align : {}
14
- end
15
-
16
- def get(line) = @data[line]
17
-
18
- def set(line, values) = (@data[line] = values)
19
-
20
- def persist
21
- return FileUtils.rm_f(@path) if @data.empty?
22
-
23
- output = {}
24
- @data.keys.sort.each do |line|
25
- xxh = @source.xxh(line)
26
- next unless xxh
27
-
28
- output["L#{line} #{xxh}"] = @data[line]
29
- end
30
-
31
- content = YAML.dump(output)
32
- File.write(@path, content)
33
- end
34
-
35
- # Used in Failure to get the actual storage metadata
36
- def lookup(lineno, index = 0)
37
- return unless File.exist?(@path)
38
-
39
- xxh = @source.xxh(lineno)
40
- key = "L#{lineno} #{xxh}"
41
-
42
- found = false
43
- count = -1
44
-
45
- File.foreach(@path).with_index(1) do |content, ln|
46
- if found
47
- next unless content.start_with?('-')
48
-
49
- count += 1
50
- next unless count == index
51
-
52
- return { path: @path, line: ln, key: key }
53
- else
54
- found = content.match(/\b#{xxh}\b/)
55
- end
56
- end
57
- end
58
-
59
- private
60
-
61
- def load_and_align
62
- {}.tap do |aligned|
63
- data = YAML.unsafe_load_file(@path) || {}
64
- data.group_by { |k, _| k.split.last }.each do |xxh, entries|
65
- lines = @source.lines(xxh)
66
- next if lines.empty?
67
-
68
- # Position of the held lines compared to the source lines
69
- stayed, moved = entries.map { |key, values| { line: key[/\d+/].to_i, values: } }
70
- .partition { lines.include?(_1[:line]) }
71
- moved.sort_by! { _1[:line] }
72
-
73
- # Align lines
74
- lines.each do |line|
75
- match = stayed.find { _1[:line] == line } || moved.shift
76
- aligned[line] = match[:values] if match
77
- end
78
- end
79
- end
80
- end
81
- end
82
- end