howzit 2.1.47 → 2.1.48
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 +17 -0
- data/lib/howzit/task.rb +1 -1
- data/lib/howzit/topic.rb +104 -28
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +2 -1
- data/spec/collected_notes_spec.rb +142 -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: 2a6dffed9d9ce31f4704b3388bbf330f87d86f054067444073ea34b8cba13a54
|
|
4
|
+
data.tar.gz: 87cdb843c109e8f0b88e482634d0f35b676ffcd6e2732009f5a7aa60932f7fff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e278c055f9647bb784cfac2f1fe00616dde57023a5cd08e714097ccc3f32181ad67898da2507fe23c5496a235cdee5143c29918149a0a15d1e70574e3d535d8
|
|
7
|
+
data.tar.gz: 409df1fe83a7e46ba317d2c9cceb8346ea94c7578376f1836b7d9bb6cb17b83b51020955470d1371a27abb46150ec6af49da1e5b15ac8108eff9b9fccb625cc9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
### 2.1.48
|
|
2
|
+
|
|
3
|
+
2026-10-09 07:20
|
|
4
|
+
|
|
5
|
+
#### CHANGED
|
|
6
|
+
|
|
7
|
+
- **@before blocks** from a topic, its subtopics, and any included topics are shown together in one box before anything runs, with a single confirmation prompt instead of one prompt per topic
|
|
8
|
+
- **@after blocks** from a topic, its subtopics, and any included topics are shown together in one box at the end of the run instead of after each topic finishes
|
|
9
|
+
|
|
10
|
+
#### IMPROVED
|
|
11
|
+
|
|
12
|
+
- **Collected notes** appear in document order: the topic's own block, then included topics in @include order, then subtopics
|
|
13
|
+
- **@after notes from subtopics skipped after a failure** are still shown in the final box
|
|
14
|
+
- **Notes from included topics** render variables with the bracketed arguments passed via @include(Topic [args])
|
|
15
|
+
- **@before blocks** now render ${variables}, matching @after
|
|
16
|
+
- **Topics included more than once** only contribute their notes once
|
|
17
|
+
|
|
1
18
|
### 2.1.47
|
|
2
19
|
|
|
3
20
|
2026-09-27 13:20
|
data/lib/howzit/task.rb
CHANGED
data/lib/howzit/topic.rb
CHANGED
|
@@ -165,34 +165,58 @@ module Howzit
|
|
|
165
165
|
## @return [Array] output lines
|
|
166
166
|
##
|
|
167
167
|
def run(nested: false, as_subtopic: false)
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
cols = check_cols
|
|
171
|
-
|
|
172
|
-
confirm_prereqs(cols) if !@prereqs.empty? && (all_tasks.any? || sequential?)
|
|
173
|
-
|
|
174
|
-
if sequential?
|
|
175
|
-
run_sequential(output: output)
|
|
176
|
-
elsif @tasks.any?
|
|
177
|
-
run_tasks(output)
|
|
178
|
-
elsif all_tasks.empty? && !as_subtopic && !directives?
|
|
179
|
-
Howzit.console.warn "{r}--run: No {br}@directive{xr} found in {bw}#{@title}{x}".c
|
|
180
|
-
end
|
|
168
|
+
# The outermost run shows @before/@after notes for the whole tree
|
|
169
|
+
return run_topic(nested: nested, as_subtopic: as_subtopic) if Howzit.topic_run_active
|
|
181
170
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
171
|
+
Howzit.topic_run_active = true
|
|
172
|
+
begin
|
|
173
|
+
cols = check_cols
|
|
174
|
+
confirm_prereqs(cols) if all_tasks.any? || sequential?
|
|
175
|
+
output = run_topic(nested: nested, as_subtopic: as_subtopic)
|
|
176
|
+
show_postreqs(cols)
|
|
177
|
+
output
|
|
178
|
+
ensure
|
|
179
|
+
Howzit.topic_run_active = false
|
|
186
180
|
end
|
|
181
|
+
end
|
|
187
182
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
183
|
+
##
|
|
184
|
+
## @before notes for this topic, the topics it includes, and its
|
|
185
|
+
## subtopics, in document order
|
|
186
|
+
##
|
|
187
|
+
## @return [Array] rendered notes
|
|
188
|
+
##
|
|
189
|
+
def all_prereqs
|
|
190
|
+
collect_notes(:prereqs)
|
|
191
|
+
end
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
##
|
|
194
|
+
## @after notes for this topic, the topics it includes, and its
|
|
195
|
+
## subtopics, in document order
|
|
196
|
+
##
|
|
197
|
+
## @return [Array] rendered notes
|
|
198
|
+
##
|
|
199
|
+
def all_postreqs
|
|
200
|
+
collect_notes(:postreqs)
|
|
201
|
+
end
|
|
194
202
|
|
|
195
|
-
|
|
203
|
+
##
|
|
204
|
+
## Gather notes recursively through @include tasks and subtopics
|
|
205
|
+
##
|
|
206
|
+
## @param kind [Symbol] :prereqs or :postreqs
|
|
207
|
+
## @param args [Array] Bracketed arguments from an @include
|
|
208
|
+
## @param seen [Array] Titles of topics already collected
|
|
209
|
+
##
|
|
210
|
+
def collect_notes(kind, args = nil, seen = [])
|
|
211
|
+
return [] if seen.include?(@title)
|
|
212
|
+
|
|
213
|
+
seen << @title
|
|
214
|
+
notes = render_notes(kind == :prereqs ? @prereqs : @postreqs, args)
|
|
215
|
+
@tasks.select { |task| task.type == :include }.each do |task|
|
|
216
|
+
topic = Howzit.buildnote.find_topic(task.action.sub(/ *\[.*?\] *$/, ''))[0]
|
|
217
|
+
notes.concat(topic.collect_notes(kind, task.include_args, seen)) if topic
|
|
218
|
+
end
|
|
219
|
+
notes + @subtopics.flat_map { |sub| sub.collect_notes(kind, nil, seen) }
|
|
196
220
|
end
|
|
197
221
|
|
|
198
222
|
##
|
|
@@ -215,8 +239,11 @@ module Howzit
|
|
|
215
239
|
end
|
|
216
240
|
|
|
217
241
|
def confirm_prereqs(cols)
|
|
242
|
+
prereqs = all_prereqs
|
|
243
|
+
return if prereqs.empty?
|
|
244
|
+
|
|
218
245
|
begin
|
|
219
|
-
puts TTY::Box.frame("{by}#{
|
|
246
|
+
puts TTY::Box.frame("{by}#{prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
|
|
220
247
|
rescue Errno::EPIPE
|
|
221
248
|
# Pipe closed, ignore
|
|
222
249
|
end
|
|
@@ -225,11 +252,11 @@ module Howzit
|
|
|
225
252
|
end
|
|
226
253
|
|
|
227
254
|
def show_postreqs(cols)
|
|
228
|
-
|
|
255
|
+
postreqs = all_postreqs
|
|
256
|
+
return if postreqs.empty?
|
|
229
257
|
|
|
230
|
-
#
|
|
231
|
-
|
|
232
|
-
wrapped_content = postreqs_content.split(/\n/).map { |line| line.wrap(cols - 4) }.join("\n")
|
|
258
|
+
# Wrap each line individually to preserve structure
|
|
259
|
+
wrapped_content = postreqs.join("\n\n").split(/\n/).map { |line| line.wrap(cols - 4) }.join("\n")
|
|
233
260
|
puts TTY::Box.frame("{bw}#{wrapped_content}{x}".c, width: cols)
|
|
234
261
|
rescue Errno::EPIPE
|
|
235
262
|
# Pipe closed, ignore
|
|
@@ -593,6 +620,55 @@ module Howzit
|
|
|
593
620
|
|
|
594
621
|
private
|
|
595
622
|
|
|
623
|
+
def run_topic(nested:, as_subtopic:)
|
|
624
|
+
@results = { total: 0, success: 0, errors: 0, message: ''.c }
|
|
625
|
+
output = []
|
|
626
|
+
|
|
627
|
+
if sequential?
|
|
628
|
+
run_sequential(output: output)
|
|
629
|
+
elsif @tasks.any?
|
|
630
|
+
run_tasks(output)
|
|
631
|
+
elsif all_tasks.empty? && !as_subtopic && !directives?
|
|
632
|
+
Howzit.console.warn "{r}--run: No {br}@directive{xr} found in {bw}#{@title}{x}".c
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
if halted?
|
|
636
|
+
log_skipped_tasks(@subtopics.flat_map(&:all_tasks))
|
|
637
|
+
else
|
|
638
|
+
run_subtopics(output)
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
if @results[:total].positive? || (all_tasks.any? && !sequential?)
|
|
642
|
+
@results[:message] += results_message
|
|
643
|
+
output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
output
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
##
|
|
650
|
+
## Render notes with this topic's variables, plus any bracketed
|
|
651
|
+
## @include arguments bound to its title parameters
|
|
652
|
+
##
|
|
653
|
+
def render_notes(notes, args)
|
|
654
|
+
return [] if notes.empty?
|
|
655
|
+
|
|
656
|
+
previous = Howzit.named_arguments
|
|
657
|
+
begin
|
|
658
|
+
Howzit.named_arguments = (previous || {}).merge(@named_args.compact).merge(bound_arguments(args))
|
|
659
|
+
notes.map(&:render_arguments)
|
|
660
|
+
ensure
|
|
661
|
+
Howzit.named_arguments = previous
|
|
662
|
+
end
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def bound_arguments(args)
|
|
666
|
+
return {} if args.nil? || args.empty?
|
|
667
|
+
|
|
668
|
+
names = @arg_definitions.map { |definition| definition.split(':', 2).first }
|
|
669
|
+
names.zip(args).to_h.compact
|
|
670
|
+
end
|
|
671
|
+
|
|
596
672
|
##
|
|
597
673
|
## Collect all directives in the topic content
|
|
598
674
|
##
|
data/lib/howzit/version.rb
CHANGED
data/lib/howzit.rb
CHANGED
|
@@ -60,7 +60,8 @@ require 'tty/box'
|
|
|
60
60
|
# Main module for howzit
|
|
61
61
|
module Howzit
|
|
62
62
|
class << self
|
|
63
|
-
attr_accessor :arguments, :named_arguments, :cli_topic_positional_args, :cli_args, :run_log, :multi_topic_run
|
|
63
|
+
attr_accessor :arguments, :named_arguments, :cli_topic_positional_args, :cli_args, :run_log, :multi_topic_run,
|
|
64
|
+
:topic_run_active
|
|
64
65
|
|
|
65
66
|
##
|
|
66
67
|
## Holds a Configuration object with methods and a @settings hash
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require 'stringio'
|
|
7
|
+
|
|
8
|
+
describe 'collected @before/@after notes' do
|
|
9
|
+
let(:temp_dir) { File.expand_path(Dir.mktmpdir('howzit_notes_test')) }
|
|
10
|
+
let(:out_file) { File.join(temp_dir, 'out.txt') }
|
|
11
|
+
let(:child_two_command) { 'false' }
|
|
12
|
+
let(:buildnote) do
|
|
13
|
+
note = Howzit::BuildNote.new
|
|
14
|
+
allow(note).to receive(:finalize_output)
|
|
15
|
+
note
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
before do
|
|
19
|
+
File.write(
|
|
20
|
+
File.join(temp_dir, 'buildnotes.md'),
|
|
21
|
+
<<~NOTE
|
|
22
|
+
# Notes Project
|
|
23
|
+
|
|
24
|
+
## Parent
|
|
25
|
+
|
|
26
|
+
@before
|
|
27
|
+
Parent before
|
|
28
|
+
@end
|
|
29
|
+
|
|
30
|
+
@run(echo "parent" >> "#{out_file}") Parent task
|
|
31
|
+
|
|
32
|
+
@include(Shared [prod])
|
|
33
|
+
|
|
34
|
+
@after
|
|
35
|
+
Parent after
|
|
36
|
+
@end
|
|
37
|
+
|
|
38
|
+
### Child One
|
|
39
|
+
|
|
40
|
+
@before
|
|
41
|
+
Child before
|
|
42
|
+
@end
|
|
43
|
+
|
|
44
|
+
@run(echo "child" >> "#{out_file}") Child task
|
|
45
|
+
|
|
46
|
+
@after
|
|
47
|
+
Child after
|
|
48
|
+
@end
|
|
49
|
+
|
|
50
|
+
### Child Two
|
|
51
|
+
|
|
52
|
+
@run(#{child_two_command}) Child two task
|
|
53
|
+
|
|
54
|
+
### Child Three
|
|
55
|
+
|
|
56
|
+
@run(echo "three" >> "#{out_file}") Three task
|
|
57
|
+
|
|
58
|
+
@after
|
|
59
|
+
Three after
|
|
60
|
+
@end
|
|
61
|
+
|
|
62
|
+
## Shared (target)
|
|
63
|
+
|
|
64
|
+
@run(echo "shared ${target}" >> "#{out_file}") Shared task
|
|
65
|
+
|
|
66
|
+
@after
|
|
67
|
+
Shared after ${target}
|
|
68
|
+
@end
|
|
69
|
+
NOTE
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
Dir.chdir(temp_dir)
|
|
73
|
+
Howzit.instance_variable_set(:@buildnote, buildnote)
|
|
74
|
+
Howzit.options[:stack] = false
|
|
75
|
+
Howzit.options[:include_upstream] = false
|
|
76
|
+
Howzit.options[:run] = true
|
|
77
|
+
Howzit.options[:force] = false
|
|
78
|
+
Howzit.options[:log_level] = 3
|
|
79
|
+
Howzit.cli_args = ['parent']
|
|
80
|
+
Howzit.run_log = []
|
|
81
|
+
allow(Howzit::Prompt).to receive(:yn).and_return(true)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
after do
|
|
85
|
+
Howzit.options[:run] = false
|
|
86
|
+
Howzit.options[:log_level] = 1
|
|
87
|
+
Howzit.cli_args = []
|
|
88
|
+
Howzit.run_log = []
|
|
89
|
+
Dir.chdir(Dir.tmpdir)
|
|
90
|
+
FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
|
|
91
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def run_and_capture
|
|
95
|
+
original = $stdout
|
|
96
|
+
$stdout = StringIO.new
|
|
97
|
+
buildnote.run
|
|
98
|
+
$stdout.string.gsub(/\e\[[\d;]*m/, '')
|
|
99
|
+
ensure
|
|
100
|
+
$stdout = original
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def positions(text, notes)
|
|
104
|
+
notes.map { |note| text.index(note) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'asks once for all prerequisites before running anything' do
|
|
108
|
+
text = run_and_capture
|
|
109
|
+
|
|
110
|
+
expect(Howzit::Prompt).to have_received(:yn).once
|
|
111
|
+
expect(positions(text, ['Parent before', 'Child before'])).to all(be_a(Integer))
|
|
112
|
+
expect(text.scan('Parent before').count).to eq 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'shows all @after notes together in document order, including included topics' do
|
|
116
|
+
text = run_and_capture
|
|
117
|
+
|
|
118
|
+
notes = ['Parent after', 'Shared after prod', 'Child after', 'Three after']
|
|
119
|
+
found = positions(text, notes)
|
|
120
|
+
expect(found).to all(be_a(Integer))
|
|
121
|
+
expect(found).to eq(found.sort)
|
|
122
|
+
expect(text.scan('Child after').count).to eq 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it 'shows notes from subtopics skipped after a failure' do
|
|
126
|
+
text = run_and_capture
|
|
127
|
+
|
|
128
|
+
expect(File.read(out_file).lines.map(&:strip)).to eq(['parent', 'shared prod', 'child'])
|
|
129
|
+
expect(text).to include('Three after')
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
context 'when nothing fails' do
|
|
133
|
+
let(:child_two_command) { 'true' }
|
|
134
|
+
|
|
135
|
+
it 'shows the combined @after box after all tasks have run' do
|
|
136
|
+
text = run_and_capture
|
|
137
|
+
|
|
138
|
+
expect(File.read(out_file).lines.map(&:strip)).to eq(['parent', 'shared prod', 'child', 'three'])
|
|
139
|
+
expect(text.scan('Parent after').count).to eq 1
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: howzit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
4
|
+
version: 2.1.48
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
@@ -317,6 +317,7 @@ files:
|
|
|
317
317
|
- scripts/runtests.sh
|
|
318
318
|
- spec/buildnote_spec.rb
|
|
319
319
|
- spec/cli_spec.rb
|
|
320
|
+
- spec/collected_notes_spec.rb
|
|
320
321
|
- spec/condition_evaluator_spec.rb
|
|
321
322
|
- spec/conditional_blocks_integration_spec.rb
|
|
322
323
|
- spec/conditional_content_spec.rb
|
|
@@ -367,6 +368,7 @@ summary: Provides a way to access Markdown project notes by topic with query cap
|
|
|
367
368
|
test_files:
|
|
368
369
|
- spec/buildnote_spec.rb
|
|
369
370
|
- spec/cli_spec.rb
|
|
371
|
+
- spec/collected_notes_spec.rb
|
|
370
372
|
- spec/condition_evaluator_spec.rb
|
|
371
373
|
- spec/conditional_blocks_integration_spec.rb
|
|
372
374
|
- spec/conditional_content_spec.rb
|