howzit 2.1.21 → 2.1.23
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/CHANGELOG.md +32 -0
- data/lib/howzit/buildnote.rb +83 -11
- data/lib/howzit/prompt.rb +188 -0
- data/lib/howzit/run_report.rb +47 -20
- data/lib/howzit/version.rb +1 -1
- data/spec/buildnote_spec.rb +102 -0
- data/spec/run_report_spec.rb +24 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6fc52d90b2a71d349a14e77f6e313baed51003a43ce2ca615a31f0b070f0681e
|
|
4
|
+
data.tar.gz: fa0fa63742822ef396b45abc56b6ae7dfbc7acb72a4c896a9e6570cc9e13b1c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d7e01306310519e4454960b5246740572a7731aa4946fa3d4d455ee3f9a3c30b1415eabdf91a58dca48d0123a0b78332754d7e4d86bd112692574b517a328072
|
|
7
|
+
data.tar.gz: 99ab41b0612da8c5a0e456c5a5006e9dc6cf5e679726b461cba170622d3aa615fe6a259c50dec67b2b4c7151afff200af8d8cf2c3dfe1d775b0dea83e3975fba
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
### 2.1.23
|
|
2
|
+
|
|
3
|
+
2025-12-13 06:38
|
|
4
|
+
|
|
5
|
+
#### CHANGED
|
|
6
|
+
|
|
7
|
+
- Output format changed from bordered text to markdown table
|
|
8
|
+
|
|
9
|
+
#### NEW
|
|
10
|
+
|
|
11
|
+
- Added emoji header () to run report table
|
|
12
|
+
|
|
13
|
+
#### IMPROVED
|
|
14
|
+
|
|
15
|
+
- Status indicators now use emoji (/) instead of text symbols
|
|
16
|
+
- Failure messages now show "(exit code X)" instead of "Failed: exit code X"
|
|
17
|
+
|
|
18
|
+
### 2.1.22
|
|
19
|
+
|
|
20
|
+
2025-12-13 06:14
|
|
21
|
+
|
|
22
|
+
#### NEW
|
|
23
|
+
|
|
24
|
+
- Template selection menu when creating new build notes
|
|
25
|
+
- Prompt for required template variables during note creation
|
|
26
|
+
- Gum support as fallback for menus and text input
|
|
27
|
+
|
|
28
|
+
#### IMPROVED
|
|
29
|
+
|
|
30
|
+
- Fuzzy matching for template names when fzf unavailable
|
|
31
|
+
- Text input uses Readline for proper line editing (backspace, ctrl-a/e)
|
|
32
|
+
|
|
1
33
|
### 2.1.21
|
|
2
34
|
|
|
3
35
|
2025-12-13 05:03
|
data/lib/howzit/buildnote.rb
CHANGED
|
@@ -293,27 +293,37 @@ module Howzit
|
|
|
293
293
|
if default
|
|
294
294
|
input = title
|
|
295
295
|
else
|
|
296
|
-
|
|
297
|
-
printf "{bw}Project name {xg}[#{title}]{bw}: {x}".c
|
|
298
|
-
input = $stdin.gets.chomp
|
|
299
|
-
title = input unless input.empty?
|
|
296
|
+
title = Prompt.get_line('{bw}Project name{x}'.c, default: title)
|
|
300
297
|
end
|
|
301
298
|
summary = ''
|
|
302
299
|
unless default
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
300
|
+
summary = Prompt.get_line('{bw}Project summary{x}'.c)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Template selection
|
|
304
|
+
selected_templates = []
|
|
305
|
+
template_metadata = {}
|
|
306
|
+
unless default
|
|
307
|
+
selected_templates, template_metadata = select_templates_for_note(title)
|
|
306
308
|
end
|
|
307
309
|
|
|
308
310
|
fname = 'buildnotes.md'
|
|
309
311
|
unless default
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
312
|
+
fname = Prompt.get_line("{bw}Build notes filename{x}\n(must begin with 'howzit' or 'build')".c, default: fname)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Build metadata section
|
|
316
|
+
metadata_lines = []
|
|
317
|
+
unless selected_templates.empty?
|
|
318
|
+
metadata_lines << "template: #{selected_templates.join(',')}"
|
|
319
|
+
end
|
|
320
|
+
template_metadata.each do |key, value|
|
|
321
|
+
metadata_lines << "#{key}: #{value}"
|
|
313
322
|
end
|
|
323
|
+
metadata_section = metadata_lines.empty? ? '' : "#{metadata_lines.join("\n")}\n\n"
|
|
314
324
|
|
|
315
325
|
note = <<~EOBUILDNOTES
|
|
316
|
-
# #{title}
|
|
326
|
+
#{metadata_section}# #{title}
|
|
317
327
|
|
|
318
328
|
#{summary}
|
|
319
329
|
|
|
@@ -369,6 +379,68 @@ module Howzit
|
|
|
369
379
|
|
|
370
380
|
private
|
|
371
381
|
|
|
382
|
+
##
|
|
383
|
+
## Select templates for a new build note
|
|
384
|
+
##
|
|
385
|
+
## @param project_title [String] The project title for prompts
|
|
386
|
+
##
|
|
387
|
+
## @return [Array<Array, Hash>] Array of [selected_template_names, required_vars_hash]
|
|
388
|
+
##
|
|
389
|
+
def select_templates_for_note(project_title)
|
|
390
|
+
template_dir = Howzit.config.template_folder
|
|
391
|
+
template_glob = File.join(template_dir, '*.md')
|
|
392
|
+
template_files = Dir.glob(template_glob)
|
|
393
|
+
|
|
394
|
+
return [[], {}] if template_files.empty?
|
|
395
|
+
|
|
396
|
+
# Get basenames without extension for menu
|
|
397
|
+
template_names = template_files.map { |f| File.basename(f, '.md') }.sort
|
|
398
|
+
|
|
399
|
+
# Show multi-select menu
|
|
400
|
+
selected = Prompt.choose_templates(template_names, prompt_text: 'Select templates to include')
|
|
401
|
+
return [[], {}] if selected.empty?
|
|
402
|
+
|
|
403
|
+
# Prompt for required variables from each template
|
|
404
|
+
required_vars = {}
|
|
405
|
+
selected.each do |template_name|
|
|
406
|
+
template_path = File.join(template_dir, "#{template_name}.md")
|
|
407
|
+
next unless File.exist?(template_path)
|
|
408
|
+
|
|
409
|
+
vars = parse_template_required_vars(template_path)
|
|
410
|
+
vars.each do |var|
|
|
411
|
+
next if required_vars.key?(var)
|
|
412
|
+
|
|
413
|
+
value = Prompt.get_line("{bw}[#{template_name}] requires {by}#{var}{x}".c)
|
|
414
|
+
required_vars[var] = value unless value.empty?
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
[selected, required_vars]
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
##
|
|
422
|
+
## Parse a template file for required variables
|
|
423
|
+
##
|
|
424
|
+
## @param template_path [String] Path to the template file
|
|
425
|
+
##
|
|
426
|
+
## @return [Array] Array of required variable names
|
|
427
|
+
##
|
|
428
|
+
def parse_template_required_vars(template_path)
|
|
429
|
+
content = File.read(template_path)
|
|
430
|
+
|
|
431
|
+
# Look for required: in the metadata at the top of the file
|
|
432
|
+
# Metadata is before the first # heading
|
|
433
|
+
meta_section = content.split(/^#/)[0]
|
|
434
|
+
return [] if meta_section.nil? || meta_section.strip.empty?
|
|
435
|
+
|
|
436
|
+
# Find the required: line
|
|
437
|
+
match = meta_section.match(/^required:\s*(.+)$/i)
|
|
438
|
+
return [] unless match
|
|
439
|
+
|
|
440
|
+
# Split by comma and strip whitespace
|
|
441
|
+
match[1].split(',').map(&:strip).reject(&:empty?)
|
|
442
|
+
end
|
|
443
|
+
|
|
372
444
|
def topic_search_terms_from_cli
|
|
373
445
|
args = Howzit.cli_args || []
|
|
374
446
|
raw = args.join(' ').strip
|
data/lib/howzit/prompt.rb
CHANGED
|
@@ -100,6 +100,10 @@ module Howzit
|
|
|
100
100
|
return fzf_result(res)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
if Util.command_exist?('gum')
|
|
104
|
+
return gum_choose(matches, query: query, multi: true)
|
|
105
|
+
end
|
|
106
|
+
|
|
103
107
|
tty_menu(matches, query: query)
|
|
104
108
|
end
|
|
105
109
|
|
|
@@ -199,6 +203,190 @@ module Howzit
|
|
|
199
203
|
end
|
|
200
204
|
line == '' ? 1 : line.to_i
|
|
201
205
|
end
|
|
206
|
+
|
|
207
|
+
##
|
|
208
|
+
## Multi-select menu for templates
|
|
209
|
+
##
|
|
210
|
+
## @param matches [Array] The options list
|
|
211
|
+
## @param prompt_text [String] The prompt to display
|
|
212
|
+
##
|
|
213
|
+
## @return [Array] the selected results (can be empty)
|
|
214
|
+
##
|
|
215
|
+
def choose_templates(matches, prompt_text: 'Select templates')
|
|
216
|
+
return [] if matches.count.zero?
|
|
217
|
+
return [] unless $stdout.isatty
|
|
218
|
+
|
|
219
|
+
if Util.command_exist?('fzf')
|
|
220
|
+
height = matches.count + 3
|
|
221
|
+
settings = fzf_template_options(height, prompt_text: prompt_text)
|
|
222
|
+
|
|
223
|
+
# Save terminal state before fzf
|
|
224
|
+
tty_state = `stty -g`.chomp
|
|
225
|
+
res = `echo #{Shellwords.escape(matches.join("\n"))} | fzf #{settings.join(' ')}`.strip
|
|
226
|
+
# Restore terminal state after fzf
|
|
227
|
+
system("stty #{tty_state}")
|
|
228
|
+
|
|
229
|
+
return res.empty? ? [] : res.split(/\n/)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
if Util.command_exist?('gum')
|
|
233
|
+
return gum_choose(matches, prompt: prompt_text, multi: true, required: false)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
text_template_input(matches)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
##
|
|
240
|
+
## FZF options for template selection
|
|
241
|
+
##
|
|
242
|
+
def fzf_template_options(height, prompt_text: 'Select templates')
|
|
243
|
+
[
|
|
244
|
+
'-0',
|
|
245
|
+
'-m',
|
|
246
|
+
"--height=#{height}",
|
|
247
|
+
'--header="Tab: add selection, ctrl-a/d: (de)select all, esc: skip, return: confirm"',
|
|
248
|
+
'--bind ctrl-a:select-all,ctrl-d:deselect-all,ctrl-t:toggle-all',
|
|
249
|
+
"--prompt=\"#{prompt_text} > \""
|
|
250
|
+
]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
##
|
|
254
|
+
## Text-based template input with fuzzy matching
|
|
255
|
+
##
|
|
256
|
+
## @param available [Array] Available template names
|
|
257
|
+
##
|
|
258
|
+
## @return [Array] Matched template names
|
|
259
|
+
##
|
|
260
|
+
def text_template_input(available)
|
|
261
|
+
@stty_save = `stty -g`.chomp
|
|
262
|
+
|
|
263
|
+
trap('INT') do
|
|
264
|
+
system('stty', @stty_save)
|
|
265
|
+
exit
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
puts "\n{bw}Available templates:{x} #{available.join(', ')}".c
|
|
269
|
+
printf '{bw}Enter templates to include, comma-separated (return to skip):{x} '.c
|
|
270
|
+
input = Readline.readline('', true).strip
|
|
271
|
+
|
|
272
|
+
return [] if input.empty?
|
|
273
|
+
|
|
274
|
+
fuzzy_match_templates(input, available)
|
|
275
|
+
ensure
|
|
276
|
+
system('stty', @stty_save) if @stty_save
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
##
|
|
280
|
+
## Fuzzy match user input against available templates
|
|
281
|
+
##
|
|
282
|
+
## @param input [String] Comma-separated user input
|
|
283
|
+
## @param available [Array] Available template names
|
|
284
|
+
##
|
|
285
|
+
## @return [Array] Matched template names
|
|
286
|
+
##
|
|
287
|
+
def fuzzy_match_templates(input, available)
|
|
288
|
+
terms = input.split(',').map(&:strip).reject(&:empty?)
|
|
289
|
+
matched = []
|
|
290
|
+
|
|
291
|
+
terms.each do |term|
|
|
292
|
+
# Try exact match first (case-insensitive)
|
|
293
|
+
exact = available.find { |t| t.downcase == term.downcase }
|
|
294
|
+
if exact
|
|
295
|
+
matched << exact unless matched.include?(exact)
|
|
296
|
+
next
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Try fuzzy match using the same regex approach as topic matching
|
|
300
|
+
rx = term.to_rx
|
|
301
|
+
fuzzy = available.select { |t| t =~ rx }
|
|
302
|
+
|
|
303
|
+
# Prefer matches that start with the term
|
|
304
|
+
if fuzzy.length > 1
|
|
305
|
+
starts_with = fuzzy.select { |t| t.downcase.start_with?(term.downcase) }
|
|
306
|
+
fuzzy = starts_with unless starts_with.empty?
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
fuzzy.each { |t| matched << t unless matched.include?(t) }
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
matched
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
##
|
|
316
|
+
## Prompt for a single line of input
|
|
317
|
+
##
|
|
318
|
+
## @param prompt_text [String] The prompt to display
|
|
319
|
+
## @param default [String] Default value if empty
|
|
320
|
+
##
|
|
321
|
+
## @return [String] the entered value
|
|
322
|
+
##
|
|
323
|
+
def get_line(prompt_text, default: nil)
|
|
324
|
+
return (default || '') unless $stdout.isatty
|
|
325
|
+
|
|
326
|
+
if Util.command_exist?('gum')
|
|
327
|
+
result = gum_input(prompt_text, placeholder: default || '')
|
|
328
|
+
return result.empty? && default ? default : result
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
prompt_with_default = default ? "#{prompt_text} [#{default}]: " : "#{prompt_text}: "
|
|
332
|
+
result = Readline.readline(prompt_with_default, true).to_s.strip
|
|
333
|
+
result.empty? && default ? default : result
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
##
|
|
337
|
+
## Use gum for single or multi-select menu
|
|
338
|
+
##
|
|
339
|
+
## @param matches [Array] The options list
|
|
340
|
+
## @param prompt [String] The prompt text
|
|
341
|
+
## @param multi [Boolean] Allow multiple selections
|
|
342
|
+
## @param required [Boolean] Require at least one selection
|
|
343
|
+
## @param query [String] The search term for display
|
|
344
|
+
##
|
|
345
|
+
## @return [Array] Selected items
|
|
346
|
+
##
|
|
347
|
+
def gum_choose(matches, prompt: nil, multi: false, required: true, query: nil)
|
|
348
|
+
prompt_text = prompt || (query ? "Select for '#{query}'" : 'Select')
|
|
349
|
+
args = ['gum', 'choose']
|
|
350
|
+
args << '--no-limit' if multi
|
|
351
|
+
args << "--header=#{Shellwords.escape(prompt_text)}"
|
|
352
|
+
args << '--cursor.foreground=6'
|
|
353
|
+
args << '--selected.foreground=2'
|
|
354
|
+
|
|
355
|
+
tty_state = `stty -g`.chomp
|
|
356
|
+
res = `echo #{Shellwords.escape(matches.join("\n"))} | #{args.join(' ')}`.strip
|
|
357
|
+
system("stty #{tty_state}")
|
|
358
|
+
|
|
359
|
+
if res.empty?
|
|
360
|
+
if required
|
|
361
|
+
Howzit.console.info 'Cancelled'
|
|
362
|
+
Process.exit 0
|
|
363
|
+
end
|
|
364
|
+
return []
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
res.split(/\n/)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
##
|
|
371
|
+
## Use gum for text input
|
|
372
|
+
##
|
|
373
|
+
## @param prompt_text [String] The prompt to display
|
|
374
|
+
## @param placeholder [String] Placeholder text
|
|
375
|
+
##
|
|
376
|
+
## @return [String] The entered value
|
|
377
|
+
##
|
|
378
|
+
def gum_input(prompt_text, placeholder: '')
|
|
379
|
+
args = ['gum', 'input']
|
|
380
|
+
args << "--header=#{Shellwords.escape(prompt_text)}"
|
|
381
|
+
args << "--placeholder=#{Shellwords.escape(placeholder)}" unless placeholder.empty?
|
|
382
|
+
args << '--cursor.foreground=6'
|
|
383
|
+
|
|
384
|
+
tty_state = `stty -g`.chomp
|
|
385
|
+
res = `#{args.join(' ')}`.strip
|
|
386
|
+
system("stty #{tty_state}")
|
|
387
|
+
|
|
388
|
+
res
|
|
389
|
+
end
|
|
202
390
|
end
|
|
203
391
|
end
|
|
204
392
|
end
|
data/lib/howzit/run_report.rb
CHANGED
|
@@ -21,31 +21,58 @@ module Howzit
|
|
|
21
21
|
def format
|
|
22
22
|
return '' if entries.empty?
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
width
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
rows = entries.map { |entry| format_row(entry, Howzit.multi_topic_run) }
|
|
25
|
+
|
|
26
|
+
# Status column: emoji + 1 space on each side = 3 chars wide visually
|
|
27
|
+
# But emojis are 2-width in terminal, so we need width of 4 for " ✅ "
|
|
28
|
+
status_width = 4
|
|
29
|
+
task_width = [4, rows.map { |r| r[:task_plain].length }.max].max
|
|
30
|
+
|
|
31
|
+
# Build the table with emoji header
|
|
32
|
+
header = "| 🚥 | #{'Task'.ljust(task_width)} |"
|
|
33
|
+
separator = "| #{':' + '-' * 2 + ':'} | #{':' + '-' * (task_width - 2)} |"
|
|
34
|
+
|
|
35
|
+
table_lines = [header, separator]
|
|
36
|
+
rows.each do |row|
|
|
37
|
+
table_lines << table_row_colored(row[:status], row[:task], row[:task_plain], status_width, task_width)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
table_lines.join("\n")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def table_row_colored(status, task, task_plain, status_width, task_width)
|
|
44
|
+
task_padding = task_width - task_plain.length
|
|
45
|
+
|
|
46
|
+
"| #{status} | #{task}#{' ' * task_padding} |"
|
|
34
47
|
end
|
|
35
48
|
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
def format_row(entry, prefix_topic)
|
|
50
|
+
symbol = entry[:success] ? '✅' : '❌'
|
|
51
|
+
symbol_colored = entry[:success] ? '{bg}✅{x}'.c : '{br}❌{x}'.c
|
|
52
|
+
|
|
53
|
+
task_parts = []
|
|
54
|
+
task_parts_plain = []
|
|
55
|
+
|
|
56
|
+
if prefix_topic && entry[:topic] && !entry[:topic].empty?
|
|
57
|
+
task_parts << "{bw}#{entry[:topic]}{x}: "
|
|
58
|
+
task_parts_plain << "#{entry[:topic]}: "
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
task_parts << "{by}#{entry[:task]}{x}"
|
|
62
|
+
task_parts_plain << entry[:task]
|
|
63
|
+
|
|
44
64
|
unless entry[:success]
|
|
45
65
|
reason = entry[:exit_status] ? "exit code #{entry[:exit_status]}" : 'failed'
|
|
46
|
-
|
|
66
|
+
task_parts << " {br}(#{reason}){x}"
|
|
67
|
+
task_parts_plain << " (#{reason})"
|
|
47
68
|
end
|
|
48
|
-
|
|
69
|
+
|
|
70
|
+
{
|
|
71
|
+
status: symbol_colored,
|
|
72
|
+
status_plain: symbol,
|
|
73
|
+
task: task_parts.join.c,
|
|
74
|
+
task_plain: task_parts_plain.join
|
|
75
|
+
}
|
|
49
76
|
end
|
|
50
77
|
end
|
|
51
78
|
end
|
data/lib/howzit/version.rb
CHANGED
data/spec/buildnote_spec.rb
CHANGED
|
@@ -134,4 +134,106 @@ describe Howzit::BuildNote do
|
|
|
134
134
|
expect(how.send(:topic_search_terms_from_cli)).to eq(['release, deploy', 'topic balogna'])
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
|
+
|
|
138
|
+
describe "#collect_topic_matches" do
|
|
139
|
+
before do
|
|
140
|
+
Howzit.options[:multiple_matches] = :first
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "collects matches for multiple search terms" do
|
|
144
|
+
search_terms = ['topic tropic', 'topic banana']
|
|
145
|
+
output = []
|
|
146
|
+
matches = how.send(:collect_topic_matches, search_terms, output)
|
|
147
|
+
expect(matches.count).to eq 2
|
|
148
|
+
expect(matches.map(&:title)).to include('Topic Tropic', 'Topic Banana')
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "prefers exact matches over fuzzy matches" do
|
|
152
|
+
# 'Topic Banana' should exact-match, not fuzzy match to multiple
|
|
153
|
+
search_terms = ['topic banana']
|
|
154
|
+
output = []
|
|
155
|
+
matches = how.send(:collect_topic_matches, search_terms, output)
|
|
156
|
+
expect(matches.count).to eq 1
|
|
157
|
+
expect(matches[0].title).to eq 'Topic Banana'
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "falls back to fuzzy match when no exact match" do
|
|
161
|
+
Howzit.options[:matching] = 'fuzzy'
|
|
162
|
+
search_terms = ['trpc'] # fuzzy for 'tropic'
|
|
163
|
+
output = []
|
|
164
|
+
matches = how.send(:collect_topic_matches, search_terms, output)
|
|
165
|
+
expect(matches.count).to eq 1
|
|
166
|
+
expect(matches[0].title).to eq 'Topic Tropic'
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "adds error message for unmatched terms" do
|
|
170
|
+
search_terms = ['nonexistent topic xyz']
|
|
171
|
+
output = []
|
|
172
|
+
matches = how.send(:collect_topic_matches, search_terms, output)
|
|
173
|
+
expect(matches.count).to eq 0
|
|
174
|
+
expect(output.join).to match(/no topic match found/i)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "collects multiple topics from comma-separated input" do
|
|
178
|
+
Howzit.cli_args = ['topic tropic,topic banana']
|
|
179
|
+
search_terms = how.send(:topic_search_terms_from_cli)
|
|
180
|
+
output = []
|
|
181
|
+
matches = how.send(:collect_topic_matches, search_terms, output)
|
|
182
|
+
expect(matches.count).to eq 2
|
|
183
|
+
Howzit.cli_args = []
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
describe "#smart_split_topics" do
|
|
188
|
+
it "splits on comma when not part of topic title" do
|
|
189
|
+
result = how.send(:smart_split_topics, 'topic tropic,topic banana')
|
|
190
|
+
expect(result).to eq(['topic tropic', 'topic banana'])
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "preserves comma when part of topic title" do
|
|
194
|
+
result = how.send(:smart_split_topics, 'release, deploy,topic banana')
|
|
195
|
+
expect(result).to eq(['release, deploy', 'topic banana'])
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "preserves colon when part of topic title" do
|
|
199
|
+
result = how.send(:smart_split_topics, 'git:clean,blog:update post')
|
|
200
|
+
expect(result).to eq(['git:clean', 'blog:update post'])
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it "handles mixed separators correctly" do
|
|
204
|
+
result = how.send(:smart_split_topics, 'git:clean:topic tropic')
|
|
205
|
+
expect(result).to eq(['git:clean', 'topic tropic'])
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
describe "#parse_template_required_vars" do
|
|
210
|
+
let(:template_with_required) do
|
|
211
|
+
Tempfile.new(['template', '.md']).tap do |f|
|
|
212
|
+
f.write("required: repo_url, author\n\n# Template\n\n## Section")
|
|
213
|
+
f.close
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
let(:template_without_required) do
|
|
218
|
+
Tempfile.new(['template', '.md']).tap do |f|
|
|
219
|
+
f.write("# Template\n\n## Section")
|
|
220
|
+
f.close
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
after do
|
|
225
|
+
template_with_required.unlink
|
|
226
|
+
template_without_required.unlink
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
it "parses required variables from template metadata" do
|
|
230
|
+
vars = how.send(:parse_template_required_vars, template_with_required.path)
|
|
231
|
+
expect(vars).to eq(['repo_url', 'author'])
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
it "returns empty array when no required metadata" do
|
|
235
|
+
vars = how.send(:parse_template_required_vars, template_without_required.path)
|
|
236
|
+
expect(vars).to eq([])
|
|
237
|
+
end
|
|
238
|
+
end
|
|
137
239
|
end
|
data/spec/run_report_spec.rb
CHANGED
|
@@ -13,14 +13,14 @@ describe Howzit::RunReport do
|
|
|
13
13
|
Howzit.multi_topic_run = false
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
it 'renders a
|
|
16
|
+
it 'renders a markdown table for single topic runs' do
|
|
17
17
|
Howzit::RunReport.log({ topic: 'Git: Config', task: 'Run Git Origin', success: true, exit_status: 0 })
|
|
18
18
|
plain = Howzit::RunReport.format.uncolor
|
|
19
|
-
expect(plain).to include('
|
|
19
|
+
expect(plain).to include('| 🚥 |')
|
|
20
|
+
expect(plain).to include('| Task')
|
|
21
|
+
expect(plain).to include('✅')
|
|
22
|
+
expect(plain).to include('Run Git Origin')
|
|
20
23
|
expect(plain).not_to include('Git: Config:')
|
|
21
|
-
top, line, bottom = plain.split("\n")
|
|
22
|
-
expect(top).to eq('=' * line.length)
|
|
23
|
-
expect(bottom).to eq('-' * line.length)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
it 'prefixes topic titles and shows failures when multiple topics run' do
|
|
@@ -28,8 +28,25 @@ describe Howzit::RunReport do
|
|
|
28
28
|
Howzit::RunReport.log({ topic: 'Git: Config', task: 'Run Git Origin', success: true, exit_status: 0 })
|
|
29
29
|
Howzit::RunReport.log({ topic: 'Git: Clean Repo', task: 'Clean Git Repo', success: false, exit_status: 12 })
|
|
30
30
|
plain = Howzit::RunReport.format.uncolor
|
|
31
|
-
expect(plain).to include('
|
|
32
|
-
expect(plain).to include('
|
|
31
|
+
expect(plain).to include('✅')
|
|
32
|
+
expect(plain).to include('Git: Config: Run Git Origin')
|
|
33
|
+
expect(plain).to include('❌')
|
|
34
|
+
expect(plain).to include('Git: Clean Repo: Clean Git Repo')
|
|
35
|
+
expect(plain).to include('exit code 12')
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'formats as a proper markdown table with aligned columns' do
|
|
39
|
+
Howzit::RunReport.log({ topic: 'Test', task: 'Short', success: true, exit_status: 0 })
|
|
40
|
+
Howzit::RunReport.log({ topic: 'Test', task: 'A much longer task name', success: true, exit_status: 0 })
|
|
41
|
+
plain = Howzit::RunReport.format.uncolor
|
|
42
|
+
lines = plain.split("\n")
|
|
43
|
+
# All lines should start and end with pipe
|
|
44
|
+
lines.each do |line|
|
|
45
|
+
expect(line).to start_with('|')
|
|
46
|
+
expect(line).to end_with('|')
|
|
47
|
+
end
|
|
48
|
+
# Second line should be separator
|
|
49
|
+
expect(lines[1]).to match(/^\|[\s:-]+\|[\s:-]+\|$/)
|
|
33
50
|
end
|
|
34
51
|
end
|
|
35
52
|
|