rfix 2.0.4 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/exe/rfix +11 -90
  3. data/lib/rfix.rb +10 -9
  4. data/lib/rfix/branch/reference.rb +2 -2
  5. data/lib/rfix/branch/upstream.rb +2 -4
  6. data/lib/rfix/cli/command.rb +14 -1
  7. data/lib/rfix/cli/command/all.rb +21 -0
  8. data/lib/rfix/cli/command/base.rb +30 -19
  9. data/lib/rfix/cli/command/branch.rb +2 -0
  10. data/lib/rfix/cli/command/help.rb +2 -0
  11. data/lib/rfix/cli/command/info.rb +6 -1
  12. data/lib/rfix/cli/command/local.rb +2 -0
  13. data/lib/rfix/cli/command/origin.rb +2 -0
  14. data/lib/rfix/cli/command/setup.rb +2 -0
  15. data/lib/rfix/cli/command/status.rb +39 -0
  16. data/lib/rfix/collector.rb +69 -0
  17. data/lib/rfix/diff.rb +69 -0
  18. data/lib/rfix/extension/comment_config.rb +15 -0
  19. data/lib/rfix/extension/offense.rb +17 -14
  20. data/lib/rfix/extension/pastel.rb +7 -4
  21. data/lib/rfix/extension/progresbar.rb +15 -0
  22. data/lib/rfix/extension/strings.rb +10 -2
  23. data/lib/rfix/file.rb +5 -3
  24. data/lib/rfix/file/base.rb +21 -14
  25. data/lib/rfix/file/deleted.rb +2 -0
  26. data/lib/rfix/file/ignored.rb +2 -0
  27. data/lib/rfix/file/null.rb +17 -0
  28. data/lib/rfix/file/tracked.rb +39 -23
  29. data/lib/rfix/file/undefined.rb +17 -0
  30. data/lib/rfix/file/untracked.rb +3 -1
  31. data/lib/rfix/formatter.rb +67 -71
  32. data/lib/rfix/highlighter.rb +1 -3
  33. data/lib/rfix/rake/gemfile.rb +26 -23
  34. data/lib/rfix/repository.rb +59 -96
  35. data/lib/rfix/types.rb +24 -14
  36. data/lib/rfix/version.rb +1 -1
  37. data/rfix.gemspec +11 -3
  38. data/vendor/cli-ui/Gemfile +17 -0
  39. data/vendor/cli-ui/Gemfile.lock +60 -0
  40. data/vendor/cli-ui/LICENSE.txt +21 -0
  41. data/vendor/cli-ui/README.md +224 -0
  42. data/vendor/cli-ui/Rakefile +20 -0
  43. data/vendor/cli-ui/bin/console +14 -0
  44. data/vendor/cli-ui/cli-ui.gemspec +25 -0
  45. data/vendor/cli-ui/dev.yml +14 -0
  46. data/vendor/cli-ui/lib/cli/ui.rb +233 -0
  47. data/vendor/cli-ui/lib/cli/ui/ansi.rb +157 -0
  48. data/vendor/cli-ui/lib/cli/ui/color.rb +84 -0
  49. data/vendor/cli-ui/lib/cli/ui/formatter.rb +192 -0
  50. data/vendor/cli-ui/lib/cli/ui/frame.rb +269 -0
  51. data/vendor/cli-ui/lib/cli/ui/frame/frame_stack.rb +98 -0
  52. data/vendor/cli-ui/lib/cli/ui/frame/frame_style.rb +120 -0
  53. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/box.rb +166 -0
  54. data/vendor/cli-ui/lib/cli/ui/frame/frame_style/bracket.rb +139 -0
  55. data/vendor/cli-ui/lib/cli/ui/glyph.rb +84 -0
  56. data/vendor/cli-ui/lib/cli/ui/os.rb +67 -0
  57. data/vendor/cli-ui/lib/cli/ui/printer.rb +59 -0
  58. data/vendor/cli-ui/lib/cli/ui/progress.rb +90 -0
  59. data/vendor/cli-ui/lib/cli/ui/prompt.rb +297 -0
  60. data/vendor/cli-ui/lib/cli/ui/prompt/interactive_options.rb +484 -0
  61. data/vendor/cli-ui/lib/cli/ui/prompt/options_handler.rb +29 -0
  62. data/vendor/cli-ui/lib/cli/ui/spinner.rb +66 -0
  63. data/vendor/cli-ui/lib/cli/ui/spinner/async.rb +40 -0
  64. data/vendor/cli-ui/lib/cli/ui/spinner/spin_group.rb +263 -0
  65. data/vendor/cli-ui/lib/cli/ui/stdout_router.rb +232 -0
  66. data/vendor/cli-ui/lib/cli/ui/terminal.rb +46 -0
  67. data/vendor/cli-ui/lib/cli/ui/truncater.rb +102 -0
  68. data/vendor/cli-ui/lib/cli/ui/version.rb +5 -0
  69. data/vendor/cli-ui/lib/cli/ui/widgets.rb +77 -0
  70. data/vendor/cli-ui/lib/cli/ui/widgets/base.rb +27 -0
  71. data/vendor/cli-ui/lib/cli/ui/widgets/status.rb +61 -0
  72. data/vendor/cli-ui/lib/cli/ui/wrap.rb +56 -0
  73. data/vendor/cli-ui/test/cli/ui/ansi_test.rb +32 -0
  74. data/vendor/cli-ui/test/cli/ui/cli_ui_test.rb +23 -0
  75. data/vendor/cli-ui/test/cli/ui/color_test.rb +40 -0
  76. data/vendor/cli-ui/test/cli/ui/formatter_test.rb +79 -0
  77. data/vendor/cli-ui/test/cli/ui/glyph_test.rb +68 -0
  78. data/vendor/cli-ui/test/cli/ui/printer_test.rb +103 -0
  79. data/vendor/cli-ui/test/cli/ui/progress_test.rb +46 -0
  80. data/vendor/cli-ui/test/cli/ui/prompt/options_handler_test.rb +39 -0
  81. data/vendor/cli-ui/test/cli/ui/prompt_test.rb +348 -0
  82. data/vendor/cli-ui/test/cli/ui/spinner/spin_group_test.rb +39 -0
  83. data/vendor/cli-ui/test/cli/ui/spinner_test.rb +141 -0
  84. data/vendor/cli-ui/test/cli/ui/stdout_router_test.rb +32 -0
  85. data/vendor/cli-ui/test/cli/ui/terminal_test.rb +26 -0
  86. data/vendor/cli-ui/test/cli/ui/truncater_test.rb +31 -0
  87. data/vendor/cli-ui/test/cli/ui/widgets/status_test.rb +49 -0
  88. data/vendor/cli-ui/test/cli/ui/widgets_test.rb +15 -0
  89. data/vendor/cli-ui/test/test_helper.rb +53 -0
  90. data/vendor/cli-ui/tmp/cache/bootsnap/compile-cache/d9/c036af0f3dc494 +0 -0
  91. data/vendor/cli-ui/tmp/cache/bootsnap/load-path-cache +0 -0
  92. data/vendor/dry-cli/lib/dry/cli/command.rb +2 -1
  93. data/vendor/dry-cli/tmp/cache/bootsnap/compile-cache/ff/a22a5daafbd74c +0 -0
  94. data/vendor/dry-cli/tmp/cache/bootsnap/load-path-cache +0 -0
  95. data/vendor/strings-ansi/tmp/cache/bootsnap/compile-cache/79/49cf49407b370e +0 -0
  96. data/vendor/strings-ansi/tmp/cache/bootsnap/load-path-cache +0 -0
  97. metadata +170 -9
  98. data/lib/rfix/extension/string.rb +0 -12
  99. data/lib/rfix/indicator.rb +0 -19
@@ -0,0 +1,484 @@
1
+ # coding: utf-8
2
+ require 'io/console'
3
+
4
+ module CLI
5
+ module UI
6
+ module Prompt
7
+ class InteractiveOptions
8
+ DONE = 'Done'
9
+ CHECKBOX_ICON = { false => '☐', true => '☑' }
10
+
11
+ # Prompts the user with options
12
+ # Uses an interactive session to allow the user to pick an answer
13
+ # Can use arrows, y/n, numbers (1/2), and vim bindings to control
14
+ # For more than 9 options, hitting 'e', ':', or 'G' will enter select
15
+ # mode allowing the user to type in longer numbers
16
+ # Pressing 'f' or '/' will allow the user to filter the results
17
+ #
18
+ # https://user-images.githubusercontent.com/3074765/33797984-0ebb5e64-dcdf-11e7-9e7e-7204f279cece.gif
19
+ #
20
+ # ==== Example Usage:
21
+ #
22
+ # Ask an interactive question
23
+ # CLI::UI::Prompt::InteractiveOptions.call(%w(rails go python))
24
+ #
25
+ def self.call(options, multiple: false, default: nil)
26
+ list = new(options, multiple: multiple, default: default)
27
+ selected = list.call
28
+ if multiple
29
+ selected.map { |s| options[s - 1] }
30
+ else
31
+ options[selected - 1]
32
+ end
33
+ end
34
+
35
+ # Initializes a new +InteractiveOptions+
36
+ # Usually called from +self.call+
37
+ #
38
+ # ==== Example Usage:
39
+ #
40
+ # CLI::UI::Prompt::InteractiveOptions.new(%w(rails go python))
41
+ #
42
+ def initialize(options, multiple: false, default: nil)
43
+ @options = options
44
+ @active = 1
45
+ @marker = '>'
46
+ @answer = nil
47
+ @state = :root
48
+ @multiple = multiple
49
+ # Indicate that an extra line (the "metadata" line) is present and
50
+ # the terminal output should be drawn over when processing user input
51
+ @displaying_metadata = false
52
+ @filter = ''
53
+ # 0-indexed array representing if selected
54
+ # @options[0] is selected if @chosen[0]
55
+ if multiple
56
+ @chosen = if default
57
+ @options.map { |option| default.include?(option) }
58
+ else
59
+ Array.new(@options.size) { false }
60
+ end
61
+ end
62
+ @redraw = true
63
+ @presented_options = []
64
+ end
65
+
66
+ # Calls the +InteractiveOptions+ and asks the question
67
+ # Usually used from +self.call+
68
+ #
69
+ def call
70
+ calculate_option_line_lengths
71
+ CLI::UI.raw { print(ANSI.hide_cursor) }
72
+ while @answer.nil?
73
+ render_options
74
+ process_input_until_redraw_required
75
+ reset_position
76
+ end
77
+ clear_output
78
+
79
+ @answer
80
+ ensure
81
+ CLI::UI.raw do
82
+ print(ANSI.show_cursor)
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ def calculate_option_line_lengths
89
+ @terminal_width_at_calculation_time = CLI::UI::Terminal.width
90
+ # options will be an array of questions but each option can be multi-line
91
+ # so to get the # of lines, you need to join then split
92
+
93
+ # since lines may be longer than the terminal is wide, we need to
94
+ # determine how many extra lines would be taken up by them
95
+ max_width = (@terminal_width_at_calculation_time -
96
+ @options.count.to_s.size - # Width of the displayed number
97
+ 5 - # Extra characters added during rendering
98
+ (@multiple ? 1 : 0) # Space for the checkbox, if rendered
99
+ ).to_f
100
+
101
+ @option_lengths = @options.map do |text|
102
+ width = 1 if text.empty?
103
+ width ||= text
104
+ .split("\n")
105
+ .reject(&:empty?)
106
+ .map { |l| (CLI::UI.fmt(l, enable_color: false).length / max_width).ceil }
107
+ .reduce(&:+)
108
+
109
+ width
110
+ end
111
+ end
112
+
113
+ def reset_position(number_of_lines = num_lines)
114
+ # This will put us back at the beginning of the options
115
+ # When we redraw the options, they will be overwritten
116
+ CLI::UI.raw do
117
+ number_of_lines.times { print(ANSI.previous_line) }
118
+ end
119
+ end
120
+
121
+ def clear_output(number_of_lines = num_lines)
122
+ CLI::UI.raw do
123
+ # Write over all lines with whitespace
124
+ number_of_lines.times { puts(' ' * CLI::UI::Terminal.width) }
125
+ end
126
+ reset_position(number_of_lines)
127
+
128
+ # Update if metadata is being displayed
129
+ # This must be done _after_ the output is cleared or it won't draw over
130
+ # the entire output
131
+ @displaying_metadata = display_metadata?
132
+ end
133
+
134
+ # Don't use this in place of +@displaying_metadata+, this updates too
135
+ # quickly to be useful when drawing to the screen.
136
+ def display_metadata?
137
+ filtering? || selecting? || has_filter?
138
+ end
139
+
140
+ def num_lines
141
+ calculate_option_line_lengths if terminal_width_changed?
142
+
143
+ option_length = presented_options.reduce(0) do |total_length, (_, option_number)|
144
+ # Handle continuation markers and "Done" option when multiple is true
145
+ next total_length + 1 if option_number.nil? || option_number.zero?
146
+ total_length + @option_lengths[option_number - 1]
147
+ end
148
+
149
+ option_length + (@displaying_metadata ? 1 : 0)
150
+ end
151
+
152
+ def terminal_width_changed?
153
+ @terminal_width_at_calculation_time != CLI::UI::Terminal.width
154
+ end
155
+
156
+ ESC = "\e"
157
+ BACKSPACE = "\u007F"
158
+ CTRL_C = "\u0003"
159
+ CTRL_D = "\u0004"
160
+
161
+ def up
162
+ active_index = @filtered_options.index { |_, num| num == @active } || 0
163
+
164
+ previous_visible = @filtered_options[active_index - 1]
165
+ previous_visible ||= @filtered_options.last
166
+
167
+ @active = previous_visible ? previous_visible.last : -1
168
+ @redraw = true
169
+ end
170
+
171
+ def down
172
+ active_index = @filtered_options.index { |_, num| num == @active } || 0
173
+
174
+ next_visible = @filtered_options[active_index + 1]
175
+ next_visible ||= @filtered_options.first
176
+
177
+ @active = next_visible ? next_visible.last : -1
178
+ @redraw = true
179
+ end
180
+
181
+ # n is 1-indexed selection
182
+ # n == 0 if "Done" was selected in @multiple mode
183
+ def select_n(n)
184
+ if @multiple
185
+ if n == 0
186
+ @answer = []
187
+ @chosen.each_with_index do |selected, i|
188
+ @answer << i + 1 if selected
189
+ end
190
+ else
191
+ @active = n
192
+ @chosen[n - 1] = !@chosen[n - 1]
193
+ end
194
+ elsif n == 0
195
+ # Ignore pressing "0" when not in multiple mode
196
+ else
197
+ @active = n
198
+ @answer = n
199
+ end
200
+ @redraw = true
201
+ end
202
+
203
+ def select_bool(char)
204
+ return unless (@options - %w(yes no)).empty?
205
+ opt = @options.detect { |o| o.start_with?(char) }
206
+ @active = @options.index(opt) + 1
207
+ @answer = @options.index(opt) + 1
208
+ @redraw = true
209
+ end
210
+
211
+ def build_selection(char)
212
+ @active = (@active.to_s + char).to_i
213
+ @redraw = true
214
+ end
215
+
216
+ def chop_selection
217
+ @active = @active.to_s.chop.to_i
218
+ @redraw = true
219
+ end
220
+
221
+ def update_search(char)
222
+ @redraw = true
223
+
224
+ # Control+D or Backspace on empty search closes search
225
+ if (char == CTRL_D) || (@filter.empty? && (char == BACKSPACE))
226
+ @filter = ''
227
+ @state = :root
228
+ return
229
+ end
230
+
231
+ if char == BACKSPACE
232
+ @filter.chop!
233
+ else
234
+ @filter += char
235
+ end
236
+ end
237
+
238
+ def select_current
239
+ # Prevent selection of invisible options
240
+ return unless presented_options.any? { |_, num| num == @active }
241
+ select_n(@active)
242
+ end
243
+
244
+ def process_input_until_redraw_required
245
+ @redraw = false
246
+ wait_for_user_input until @redraw
247
+ end
248
+
249
+ # rubocop:disable Style/WhenThen,Layout/SpaceBeforeSemicolon,Style/Semicolon
250
+ def wait_for_user_input
251
+ char = read_char
252
+ @last_char = char
253
+
254
+ case char
255
+ when :timeout ; raise Interrupt # Timeout, use interrupt to simulate
256
+ when CTRL_C ; raise Interrupt
257
+ end
258
+
259
+ max_digit = [@options.size, 9].min.to_s
260
+ case @state
261
+ when :root
262
+ case char
263
+ when ESC ; @state = :esc
264
+ when 'k' ; up
265
+ when 'j' ; down
266
+ when 'e', ':', 'G' ; start_line_select
267
+ when 'f', '/' ; start_filter
268
+ when ('0'..max_digit) ; select_n(char.to_i)
269
+ when 'y', 'n' ; select_bool(char)
270
+ when ' ', "\r", "\n" ; select_current # <enter>
271
+ end
272
+ when :filter
273
+ case char
274
+ when ESC ; @state = :esc
275
+ when "\r", "\n" ; select_current
276
+ when "\b" ; update_search(BACKSPACE) # Happens on Windows
277
+ else ; update_search(char)
278
+ end
279
+ when :line_select
280
+ case char
281
+ when ESC ; @state = :esc
282
+ when 'k' ; up ; @state = :root
283
+ when 'j' ; down ; @state = :root
284
+ when 'e', ':', 'G', 'q' ; stop_line_select
285
+ when '0'..'9' ; build_selection(char)
286
+ when BACKSPACE ; chop_selection # Pop last input on backspace
287
+ when ' ', "\r", "\n" ; select_current
288
+ end
289
+ when :esc
290
+ case char
291
+ when '[' ; @state = :esc_bracket
292
+ else ; raise Interrupt # unhandled escape sequence.
293
+ end
294
+ when :esc_bracket
295
+ @state = has_filter? ? :filter : :root
296
+ case char
297
+ when 'A' ; up
298
+ when 'B' ; down
299
+ when 'C' ; # Ignore right key
300
+ when 'D' ; # Ignore left key
301
+ else ; raise Interrupt # unhandled escape sequence.
302
+ end
303
+ end
304
+ end
305
+ # rubocop:enable Style/WhenThen,Layout/SpaceBeforeSemicolon
306
+
307
+ def selecting?
308
+ @state == :line_select
309
+ end
310
+
311
+ def filtering?
312
+ @state == :filter
313
+ end
314
+
315
+ def has_filter?
316
+ !@filter.empty?
317
+ end
318
+
319
+ def start_filter
320
+ @state = :filter
321
+ @redraw = true
322
+ end
323
+
324
+ def start_line_select
325
+ @state = :line_select
326
+ @active = 0
327
+ @redraw = true
328
+ end
329
+
330
+ def stop_line_select
331
+ @state = :root
332
+ @active = 1 if @active.zero?
333
+ @redraw = true
334
+ end
335
+
336
+ def read_char
337
+ if $stdin.tty? && !ENV['TEST']
338
+ $stdin.getch # raw mode for tty
339
+ else
340
+ $stdin.getc
341
+ end
342
+ rescue IOError
343
+ "\e"
344
+ end
345
+
346
+ def presented_options(recalculate: false)
347
+ return @presented_options unless recalculate
348
+
349
+ @presented_options = @options.zip(1..Float::INFINITY)
350
+ if has_filter?
351
+ @presented_options.select! { |option, _| option.downcase.include?(@filter.downcase) }
352
+ end
353
+
354
+ # Used for selection purposes
355
+ @presented_options.push([DONE, 0]) if @multiple
356
+ @filtered_options = @presented_options.dup
357
+
358
+ ensure_visible_is_active if has_filter?
359
+
360
+ # Must have more lines before the selection than we can display
361
+ if distance_from_start_to_selection > max_lines
362
+ @presented_options.shift(distance_from_start_to_selection - max_lines)
363
+ ensure_first_item_is_continuation_marker
364
+ end
365
+
366
+ # Must have more lines after the selection than we can display
367
+ if distance_from_selection_to_end > max_lines
368
+ @presented_options.pop(distance_from_selection_to_end - max_lines)
369
+ ensure_last_item_is_continuation_marker
370
+ end
371
+
372
+ while num_lines > max_lines
373
+ # try to keep the selection centered in the window:
374
+ if distance_from_selection_to_end > distance_from_start_to_selection
375
+ # selection is closer to top than bottom, so trim a row from the bottom
376
+ ensure_last_item_is_continuation_marker
377
+ @presented_options.delete_at(-2)
378
+ else
379
+ # selection is closer to bottom than top, so trim a row from the top
380
+ ensure_first_item_is_continuation_marker
381
+ @presented_options.delete_at(1)
382
+ end
383
+ end
384
+
385
+ @presented_options
386
+ end
387
+
388
+ def ensure_visible_is_active
389
+ unless presented_options.any? { |_, num| num == @active }
390
+ @active = presented_options.first&.last.to_i
391
+ end
392
+ end
393
+
394
+ def distance_from_selection_to_end
395
+ @presented_options.count - index_of_active_option
396
+ end
397
+
398
+ def distance_from_start_to_selection
399
+ index_of_active_option
400
+ end
401
+
402
+ def index_of_active_option
403
+ @presented_options.index { |_, num| num == @active }.to_i
404
+ end
405
+
406
+ def ensure_last_item_is_continuation_marker
407
+ @presented_options.push(['...', nil]) if @presented_options.last.last
408
+ end
409
+
410
+ def ensure_first_item_is_continuation_marker
411
+ @presented_options.unshift(['...', nil]) if @presented_options.first.last
412
+ end
413
+
414
+ def max_lines
415
+ CLI::UI::Terminal.height - (@displaying_metadata ? 3 : 2) # Keeps a one line question visible
416
+ end
417
+
418
+ def render_options
419
+ previously_displayed_lines = num_lines
420
+
421
+ @displaying_metadata = display_metadata?
422
+
423
+ options = presented_options(recalculate: true)
424
+
425
+ clear_output(previously_displayed_lines) if previously_displayed_lines > num_lines
426
+
427
+ max_num_length = (@options.size + 1).to_s.length
428
+
429
+ metadata_text = if selecting?
430
+ select_text = @active
431
+ select_text = '{{info:e, q, or up/down anytime to exit}}' if @active == 0
432
+ "Select: #{select_text}"
433
+ elsif filtering? || has_filter?
434
+ filter_text = @filter
435
+ filter_text = '{{info:Ctrl-D anytime or Backspace now to exit}}' if @filter.empty?
436
+ "Filter: #{filter_text}"
437
+ end
438
+
439
+ if metadata_text
440
+ CLI::UI.with_frame_color(:blue) do
441
+ puts CLI::UI.fmt(" {{green:#{metadata_text}}}#{ANSI.clear_to_end_of_line}")
442
+ end
443
+ end
444
+
445
+ options.each do |choice, num|
446
+ is_chosen = @multiple && num && @chosen[num - 1] && num != 0
447
+
448
+ padding = ' ' * (max_num_length - num.to_s.length)
449
+ message = " #{num}#{num ? "." : " "}#{padding}"
450
+
451
+ format = '%s'
452
+ # If multiple, bold only selected. If not multiple, bold everything
453
+ format = "{{bold:#{format}}}" if !@multiple || is_chosen
454
+ format = "{{cyan:#{format}}}" if @multiple && is_chosen && num != @active
455
+ format = " #{format}"
456
+
457
+ message += format(format, CHECKBOX_ICON[is_chosen]) if @multiple && num && num > 0
458
+ message += format_choice(format, choice)
459
+
460
+ if num == @active
461
+
462
+ color = filtering? || selecting? ? 'green' : 'blue'
463
+ message = message.split("\n").map { |l| "{{#{color}:> #{l.strip}}}" }.join("\n")
464
+ end
465
+
466
+ CLI::UI.with_frame_color(:blue) do
467
+ puts CLI::UI.fmt(message)
468
+ end
469
+ end
470
+ end
471
+
472
+ def format_choice(format, choice)
473
+ eol = CLI::UI::ANSI.clear_to_end_of_line
474
+ lines = choice.split("\n")
475
+
476
+ return eol if lines.empty? # Handle blank options
477
+
478
+ lines.map! { |l| format(format, l) + eol }
479
+ lines.join("\n")
480
+ end
481
+ end
482
+ end
483
+ end
484
+ end