howzit 2.1.42 → 2.1.44
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 +52 -0
- data/Rakefile +3 -0
- data/howzit.gemspec +1 -1
- data/lib/howzit/buildnote.rb +53 -18
- data/lib/howzit/task.rb +2 -2
- data/lib/howzit/topic.rb +150 -93
- data/lib/howzit/version.rb +1 -1
- data/spec/subtopics_spec.rb +139 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 881188642b99f4be83a7697440251ec8890e6d847468c3984a72183939196857
|
|
4
|
+
data.tar.gz: c622b23318a075aa277a0a197e1ec93d8023e52e99018557e7eb6f8309c76927
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 219ae789410d1ec44619f3bfb00b7f8b539a33632e9b9d9e778f384205e03c653efa83a7a090d469dd769a2cdda53ae7026e40b495363879e59f24be38da09ae
|
|
7
|
+
data.tar.gz: d0c1dd2d03ab11923e93440fb9f04bfdfb0292d63522fd21483a38a7d2ca31c691dc64e4a894063944b586ef1b2af66fd8a4bd55ea132ce743b6e69e7fd812db
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
### 2.1.44
|
|
2
|
+
|
|
3
|
+
2026-09-27 11:38
|
|
4
|
+
|
|
5
|
+
#### CHANGED
|
|
6
|
+
|
|
7
|
+
- **Task failures** in a parent or subtopic stop the remaining subtopics from running unless --force is used.
|
|
8
|
+
- **@after blocks** on a parent are shown after the parent and all of its subtopics finish.
|
|
9
|
+
|
|
10
|
+
#### NEW
|
|
11
|
+
|
|
12
|
+
- **Subtopics**: deeper headers (### under ##, #### under ###, etc.) are now nested under the preceding shallower topic.
|
|
13
|
+
- **Displaying a parent topic** shows all of its subtopics after it, with subtopic headers drawn in a lighter style (cyan title, dotted rule).
|
|
14
|
+
- **Running a parent topic** runs its own tasks, then every subtopic's tasks in order (depth-first), with a single combined "Ran N tasks" summary.
|
|
15
|
+
|
|
16
|
+
#### IMPROVED
|
|
17
|
+
|
|
18
|
+
- **Showing all topics** (howzit with no arguments, or howzit -r with no default) only outputs top-level topics so subtopics are not repeated.
|
|
19
|
+
- **Selecting both a parent and its subtopic** outputs/runs the subtopic only once, as part of the parent.
|
|
20
|
+
- **Topic lists** (howzit -l and the runnable topics list) indent subtopics under their parents.
|
|
21
|
+
- **@include(Topic)** on a parent runs the parent and all of its subtopics, and task counts in include notes and menus now count subtopic tasks.
|
|
22
|
+
|
|
23
|
+
#### FIXED
|
|
24
|
+
|
|
25
|
+
- **Parent topics with no tasks of their own** no longer warn "No @directive found" when their subtopics contain tasks.
|
|
26
|
+
|
|
27
|
+
### 2.1.43
|
|
28
|
+
|
|
29
|
+
2026-09-27 11:35
|
|
30
|
+
|
|
31
|
+
#### CHANGED
|
|
32
|
+
|
|
33
|
+
- **Task failures** in a parent or subtopic stop the remaining subtopics from running unless --force is used.
|
|
34
|
+
- **@after blocks** on a parent are shown after the parent and all of its subtopics finish.
|
|
35
|
+
|
|
36
|
+
#### NEW
|
|
37
|
+
|
|
38
|
+
- **Subtopics**: deeper headers (### under ##, #### under ###, etc.) are now nested under the preceding shallower topic.
|
|
39
|
+
- **Displaying a parent topic** shows all of its subtopics after it, with subtopic headers drawn in a lighter style (cyan title, dotted rule).
|
|
40
|
+
- **Running a parent topic** runs its own tasks, then every subtopic's tasks in order (depth-first), with a single combined "Ran N tasks" summary.
|
|
41
|
+
|
|
42
|
+
#### IMPROVED
|
|
43
|
+
|
|
44
|
+
- **Showing all topics** (howzit with no arguments, or howzit -r with no default) only outputs top-level topics so subtopics are not repeated.
|
|
45
|
+
- **Selecting both a parent and its subtopic** outputs/runs the subtopic only once, as part of the parent.
|
|
46
|
+
- **Topic lists** (howzit -l and the runnable topics list) indent subtopics under their parents.
|
|
47
|
+
- **@include(Topic)** on a parent runs the parent and all of its subtopics, and task counts in include notes and menus now count subtopic tasks.
|
|
48
|
+
|
|
49
|
+
#### FIXED
|
|
50
|
+
|
|
51
|
+
- **Parent topics with no tasks of their own** no longer warn "No @directive found" when their subtopics contain tasks.
|
|
52
|
+
|
|
1
53
|
### 2.1.42
|
|
2
54
|
|
|
3
55
|
2026-06-29 06:52
|
data/Rakefile
CHANGED
data/howzit.gemspec
CHANGED
|
@@ -25,7 +25,7 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
|
|
26
26
|
spec.required_ruby_version = '>= 2.6.0'
|
|
27
27
|
|
|
28
|
-
spec.add_development_dependency 'bundler', '
|
|
28
|
+
spec.add_development_dependency 'bundler', '>= 2.2'
|
|
29
29
|
spec.add_development_dependency 'rake', '~> 13.0'
|
|
30
30
|
|
|
31
31
|
spec.add_development_dependency 'guard', '~> 2.11'
|
data/lib/howzit/buildnote.rb
CHANGED
|
@@ -169,11 +169,37 @@ module Howzit
|
|
|
169
169
|
output = []
|
|
170
170
|
output.push("{bg}Topics:{x}\n".c)
|
|
171
171
|
@topics.each do |topic|
|
|
172
|
-
output.push("- {bw}#{topic.title}{x}".c)
|
|
172
|
+
output.push("#{subtopic_indent(topic)}- {bw}#{topic.title}{x}".c)
|
|
173
173
|
end
|
|
174
174
|
output.join("\n")
|
|
175
175
|
end
|
|
176
176
|
|
|
177
|
+
##
|
|
178
|
+
## Indentation for a topic in list output, based on
|
|
179
|
+
## how many of its parent topics are in the list
|
|
180
|
+
##
|
|
181
|
+
## @param topic [Topic] The topic
|
|
182
|
+
##
|
|
183
|
+
## @return [String] leading whitespace
|
|
184
|
+
##
|
|
185
|
+
def subtopic_indent(topic)
|
|
186
|
+
' ' * topic.ancestors.count { |parent| @topics.any? { |t| t.equal?(parent) } }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
##
|
|
190
|
+
## Topics that aren't nested under another topic in
|
|
191
|
+
## the build note (subtopics are output with their parent)
|
|
192
|
+
##
|
|
193
|
+
## @param topics [Array] Topics to filter, defaults to all topics
|
|
194
|
+
##
|
|
195
|
+
## @return [Array] array of Topics
|
|
196
|
+
##
|
|
197
|
+
def top_level_topics(topics = @topics)
|
|
198
|
+
topics.reject do |topic|
|
|
199
|
+
topic.ancestors.any? { |parent| topics.any? { |t| t.equal?(parent) } }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
177
203
|
##
|
|
178
204
|
## Return an array of topic titles
|
|
179
205
|
##
|
|
@@ -204,7 +230,7 @@ module Howzit
|
|
|
204
230
|
def list_runnable_completions
|
|
205
231
|
output = []
|
|
206
232
|
@topics.each do |topic|
|
|
207
|
-
next unless topic.
|
|
233
|
+
next unless topic.all_tasks.any?
|
|
208
234
|
|
|
209
235
|
output.push(topic.title)
|
|
210
236
|
end
|
|
@@ -222,11 +248,10 @@ module Howzit
|
|
|
222
248
|
output.push(%({bg}"Runnable" Topics:{x}\n).c)
|
|
223
249
|
|
|
224
250
|
find_topic(Howzit.options[:for_topic]).each do |topic|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
topic.tasks.each { |task| s_out.push(task.to_list) }
|
|
251
|
+
next if topic.all_tasks.empty?
|
|
228
252
|
|
|
229
|
-
|
|
253
|
+
indent = subtopic_indent(topic)
|
|
254
|
+
s_out = topic.tasks.map { |task| "#{indent}#{task.to_list}" }
|
|
230
255
|
|
|
231
256
|
title = topic.title
|
|
232
257
|
# Show argument definitions with colorized formatting
|
|
@@ -235,8 +260,8 @@ module Howzit
|
|
|
235
260
|
title += " {l}({x}#{formatted_args}{l}){x}".c
|
|
236
261
|
end
|
|
237
262
|
|
|
238
|
-
output.push("- {g}#{title}{x}".c)
|
|
239
|
-
output.push(s_out.join("\n"))
|
|
263
|
+
output.push("#{indent}- {g}#{title}{x}".c)
|
|
264
|
+
output.push(s_out.join("\n")) unless s_out.empty?
|
|
240
265
|
end
|
|
241
266
|
|
|
242
267
|
output.join("\n")
|
|
@@ -956,11 +981,13 @@ module Howzit
|
|
|
956
981
|
|
|
957
982
|
template_topics = get_template_topics(help)
|
|
958
983
|
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
984
|
+
sections = help.split(/^(##+)/)
|
|
985
|
+
sections.shift
|
|
986
|
+
parent_stack = []
|
|
987
|
+
sections.each_slice(2) do |marks, sect|
|
|
988
|
+
next if sect.nil? || sect.strip.empty?
|
|
963
989
|
|
|
990
|
+
level = marks.length
|
|
964
991
|
lines = sect.split(/\n/)
|
|
965
992
|
title = lines.slice!(0).strip
|
|
966
993
|
prefix = ''
|
|
@@ -978,7 +1005,12 @@ module Howzit
|
|
|
978
1005
|
# Ensure source_file is always an absolute path
|
|
979
1006
|
source_file = path || note_file
|
|
980
1007
|
source_file = File.expand_path(source_file) if source_file
|
|
981
|
-
topic = Topic.new(title, prefix + lines.join("\n").strip.render_template(@metadata), @metadata,
|
|
1008
|
+
topic = Topic.new(title, prefix + lines.join("\n").strip.render_template(@metadata), @metadata,
|
|
1009
|
+
source_file: source_file, level: level)
|
|
1010
|
+
|
|
1011
|
+
parent_stack.pop while parent_stack.any? && parent_stack.last.level >= level
|
|
1012
|
+
parent_stack.last&.add_subtopic(topic)
|
|
1013
|
+
parent_stack.push(topic)
|
|
982
1014
|
|
|
983
1015
|
topics.push(topic)
|
|
984
1016
|
end
|
|
@@ -1373,13 +1405,13 @@ module Howzit
|
|
|
1373
1405
|
process_default_metadata(output)
|
|
1374
1406
|
else
|
|
1375
1407
|
Howzit.run_log = []
|
|
1376
|
-
Howzit.multi_topic_run =
|
|
1377
|
-
|
|
1408
|
+
Howzit.multi_topic_run = top_level_topics.length > 1
|
|
1409
|
+
top_level_topics.each { |k| output.push(process_topic(k, false, single: false)) }
|
|
1378
1410
|
finalize_output(output)
|
|
1379
1411
|
end
|
|
1380
1412
|
else
|
|
1381
1413
|
# Show all topics
|
|
1382
|
-
|
|
1414
|
+
top_level_topics.each { |k| output.push(process_topic(k, false, single: false)) }
|
|
1383
1415
|
finalize_output(output)
|
|
1384
1416
|
end
|
|
1385
1417
|
end
|
|
@@ -1525,9 +1557,12 @@ module Howzit
|
|
|
1525
1557
|
end
|
|
1526
1558
|
end
|
|
1527
1559
|
topic_matches.compact! # Remove any nil values from failed matches
|
|
1528
|
-
|
|
1560
|
+
# A matched subtopic is already output/run with its matched parent
|
|
1561
|
+
top_level_topics(topic_matches).each do |topic_match|
|
|
1562
|
+
output.push(process_topic(topic_match, Howzit.options[:run], single: true))
|
|
1563
|
+
end
|
|
1529
1564
|
else
|
|
1530
|
-
|
|
1565
|
+
top_level_topics.each { |k| output.push(process_topic(k, false, single: false)) }
|
|
1531
1566
|
end
|
|
1532
1567
|
|
|
1533
1568
|
finalize_output(output)
|
data/lib/howzit/task.rb
CHANGED
|
@@ -152,9 +152,9 @@ module Howzit
|
|
|
152
152
|
|
|
153
153
|
Howzit.console.info("#{@prefix}{by}Running tasks from {bw}#{matches[0].title}{x}".c)
|
|
154
154
|
output.concat(matches[0].run(nested: true))
|
|
155
|
-
Howzit.console.info("{by}End include: #{matches[0].
|
|
155
|
+
Howzit.console.info("{by}End include: #{matches[0].all_tasks.count} tasks{x}".c)
|
|
156
156
|
@last_status = nil
|
|
157
|
-
[output, matches[0].
|
|
157
|
+
[output, matches[0].all_tasks.count]
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
##
|
data/lib/howzit/topic.rb
CHANGED
|
@@ -5,9 +5,10 @@ module Howzit
|
|
|
5
5
|
class Topic
|
|
6
6
|
attr_writer :parent
|
|
7
7
|
|
|
8
|
-
attr_accessor :content
|
|
8
|
+
attr_accessor :content, :parent_topic
|
|
9
9
|
|
|
10
|
-
attr_reader :title, :tasks, :prereqs, :postreqs, :results, :named_args, :directives, :arg_definitions, :source_file
|
|
10
|
+
attr_reader :title, :tasks, :prereqs, :postreqs, :results, :named_args, :directives, :arg_definitions, :source_file,
|
|
11
|
+
:level, :subtopics
|
|
11
12
|
|
|
12
13
|
##
|
|
13
14
|
## Initialize a topic object
|
|
@@ -16,11 +17,15 @@ module Howzit
|
|
|
16
17
|
## @param content [String] The raw topic content
|
|
17
18
|
## @param metadata [Hash] Optional metadata hash
|
|
18
19
|
## @param source_file [String] Optional path to the build note file this topic came from
|
|
20
|
+
## @param level [Integer] Markdown header level (2 for ##, 3 for ###, etc.)
|
|
19
21
|
##
|
|
20
|
-
def initialize(title, content, metadata = nil, source_file: nil)
|
|
22
|
+
def initialize(title, content, metadata = nil, source_file: nil, level: 2)
|
|
21
23
|
@title = title
|
|
22
24
|
@content = content
|
|
23
25
|
@parent = nil
|
|
26
|
+
@parent_topic = nil
|
|
27
|
+
@subtopics = []
|
|
28
|
+
@level = level
|
|
24
29
|
@nest_level = 0
|
|
25
30
|
@named_args = {}
|
|
26
31
|
@metadata = metadata
|
|
@@ -32,6 +37,40 @@ module Howzit
|
|
|
32
37
|
@results = { total: 0, success: 0, errors: 0, message: ''.c }
|
|
33
38
|
end
|
|
34
39
|
|
|
40
|
+
##
|
|
41
|
+
## Nest a topic under this one (a deeper header following this topic)
|
|
42
|
+
##
|
|
43
|
+
## @param topic [Topic] The subtopic
|
|
44
|
+
##
|
|
45
|
+
def add_subtopic(topic)
|
|
46
|
+
topic.parent_topic = self
|
|
47
|
+
@subtopics << topic
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
## Tasks from this topic and all nested subtopics, in run order
|
|
52
|
+
##
|
|
53
|
+
## @return [Array] Array of Task objects
|
|
54
|
+
##
|
|
55
|
+
def all_tasks
|
|
56
|
+
@tasks + @subtopics.flat_map(&:all_tasks)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
## Parent topics from nearest to outermost
|
|
61
|
+
##
|
|
62
|
+
## @return [Array] Array of Topic objects
|
|
63
|
+
##
|
|
64
|
+
def ancestors
|
|
65
|
+
list = []
|
|
66
|
+
node = @parent_topic
|
|
67
|
+
while node
|
|
68
|
+
list << node
|
|
69
|
+
node = node.parent_topic
|
|
70
|
+
end
|
|
71
|
+
list
|
|
72
|
+
end
|
|
73
|
+
|
|
35
74
|
# Get named arguments from title
|
|
36
75
|
# from_cli_snapshot: use Howzit.cli_topic_positional_args (argv after `--`) so earlier
|
|
37
76
|
# topics' gather_tasks cannot clobber positional binding. Re-entrant @include [a,b]
|
|
@@ -80,7 +119,7 @@ module Howzit
|
|
|
80
119
|
|
|
81
120
|
def ask_task(task)
|
|
82
121
|
note = if task.type == :include
|
|
83
|
-
task_count = Howzit.buildnote.find_topic(task.action)[0].
|
|
122
|
+
task_count = Howzit.buildnote.find_topic(task.action)[0].all_tasks.count
|
|
84
123
|
" (#{task_count} tasks)"
|
|
85
124
|
else
|
|
86
125
|
''
|
|
@@ -95,80 +134,129 @@ module Howzit
|
|
|
95
134
|
60
|
|
96
135
|
end
|
|
97
136
|
|
|
98
|
-
|
|
99
|
-
|
|
137
|
+
##
|
|
138
|
+
## Handle run command, execute directives in topic, then in each subtopic
|
|
139
|
+
##
|
|
140
|
+
## @param nested [Boolean] Suppress the summary message (topic is being included)
|
|
141
|
+
## @param as_subtopic [Boolean] Topic is being run as part of a parent topic
|
|
142
|
+
##
|
|
143
|
+
## @return [Array] output lines
|
|
144
|
+
##
|
|
145
|
+
def run(nested: false, as_subtopic: false)
|
|
146
|
+
@results = { total: 0, success: 0, errors: 0, message: ''.c }
|
|
100
147
|
output = []
|
|
101
|
-
|
|
102
148
|
cols = check_cols
|
|
103
149
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
150
|
+
confirm_prereqs(cols) if !@prereqs.empty? && (all_tasks.any? || sequential?)
|
|
151
|
+
|
|
152
|
+
if sequential?
|
|
153
|
+
run_sequential(output: output)
|
|
154
|
+
elsif @tasks.any?
|
|
155
|
+
run_tasks(output)
|
|
156
|
+
elsif all_tasks.empty? && !as_subtopic && !directives?
|
|
157
|
+
Howzit.console.warn "{r}--run: No {br}@directive{xr} found in {bw}#{@title}{x}".c
|
|
107
158
|
end
|
|
108
159
|
|
|
109
|
-
|
|
110
|
-
# Note: @set_var directives are already processed in gather_tasks for non-sequential path
|
|
111
|
-
# This section is kept for backward compatibility but shouldn't be needed
|
|
160
|
+
run_subtopics(output) unless halted?
|
|
112
161
|
|
|
113
|
-
if @
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
rescue Errno::EPIPE
|
|
118
|
-
# Pipe closed, ignore
|
|
119
|
-
end
|
|
120
|
-
res = Prompt.yn('Have the above prerequisites been met?', default: true)
|
|
121
|
-
Process.exit 1 unless res
|
|
162
|
+
if @results[:total].positive? || (all_tasks.any? && !sequential?)
|
|
163
|
+
@results[:message] += results_message
|
|
164
|
+
output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
|
|
165
|
+
end
|
|
122
166
|
|
|
123
|
-
|
|
167
|
+
show_postreqs(cols)
|
|
124
168
|
|
|
125
|
-
|
|
126
|
-
|
|
169
|
+
output
|
|
170
|
+
end
|
|
127
171
|
|
|
128
|
-
|
|
172
|
+
##
|
|
173
|
+
## Whether the topic contains conditional directives and must be run sequentially
|
|
174
|
+
##
|
|
175
|
+
def sequential?
|
|
176
|
+
@directives&.any?(&:conditional?) || false
|
|
177
|
+
end
|
|
129
178
|
|
|
130
|
-
|
|
131
|
-
|
|
179
|
+
##
|
|
180
|
+
## Whether this topic or any subtopic contains directives,
|
|
181
|
+
## including non-task directives like @set_var and @log_level
|
|
182
|
+
##
|
|
183
|
+
def directives?
|
|
184
|
+
@directives&.any? || @subtopics.any?(&:directives?)
|
|
185
|
+
end
|
|
132
186
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
Howzit.console.warn %({bw}\u{2297} {br}Error running task {bw}"#{task.title}"{x}).c
|
|
187
|
+
def halted?
|
|
188
|
+
@results[:errors].positive? && !Howzit.options[:force]
|
|
189
|
+
end
|
|
137
190
|
|
|
138
|
-
|
|
191
|
+
def confirm_prereqs(cols)
|
|
192
|
+
begin
|
|
193
|
+
puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
|
|
194
|
+
rescue Errno::EPIPE
|
|
195
|
+
# Pipe closed, ignore
|
|
196
|
+
end
|
|
197
|
+
res = Prompt.yn('Have the above prerequisites been met?', default: true)
|
|
198
|
+
Process.exit 1 unless res
|
|
199
|
+
end
|
|
139
200
|
|
|
140
|
-
|
|
141
|
-
|
|
201
|
+
def show_postreqs(cols)
|
|
202
|
+
return if @postreqs.empty?
|
|
142
203
|
|
|
143
|
-
|
|
144
|
-
|
|
204
|
+
# Apply variable substitution to postreqs content, then wrap each line individually to preserve structure
|
|
205
|
+
postreqs_content = @postreqs.join("\n\n").render_arguments
|
|
206
|
+
wrapped_content = postreqs_content.split(/\n/).map { |line| line.wrap(cols - 4) }.join("\n")
|
|
207
|
+
puts TTY::Box.frame("{bw}#{wrapped_content}{x}".c, width: cols)
|
|
208
|
+
rescue Errno::EPIPE
|
|
209
|
+
# Pipe closed, ignore
|
|
210
|
+
end
|
|
145
211
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
"{br}\u{2715} {by}Ran #{total}, terminated due to error{x}".c
|
|
154
|
-
end
|
|
212
|
+
def results_message
|
|
213
|
+
total = "{bw}#{@results[:total]}{by} #{@results[:total] == 1 ? 'task' : 'tasks'}".c
|
|
214
|
+
errors = "{bw}#{@results[:errors]}{by} #{@results[:errors] == 1 ? 'error' : 'errors'}".c
|
|
215
|
+
if @results[:errors].zero?
|
|
216
|
+
"{bg}\u{2713} {by}Ran #{total}{x}".c
|
|
217
|
+
elsif Howzit.options[:force]
|
|
218
|
+
"{br}\u{2715} {by}Completed #{total} with #{errors}{x}".c
|
|
155
219
|
else
|
|
156
|
-
|
|
220
|
+
"{br}\u{2715} {by}Ran #{total}, terminated due to error{x}".c
|
|
157
221
|
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
##
|
|
225
|
+
## Run tasks without conditional evaluation
|
|
226
|
+
##
|
|
227
|
+
def run_tasks(output)
|
|
228
|
+
@tasks.each do |task|
|
|
229
|
+
next if (task.optional || Howzit.options[:ask]) && !ask_task(task)
|
|
158
230
|
|
|
159
|
-
|
|
231
|
+
run_output, total, success = task.run
|
|
232
|
+
|
|
233
|
+
output.concat(run_output)
|
|
234
|
+
@results[:total] += total
|
|
235
|
+
|
|
236
|
+
if success
|
|
237
|
+
@results[:success] += total
|
|
238
|
+
else
|
|
239
|
+
Howzit.console.warn %({bw}\u{2297} {br}Error running task {bw}"#{task.title}"{x}).c
|
|
160
240
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
postreqs_content = @postreqs.join("\n\n").render_arguments
|
|
165
|
-
wrapped_content = postreqs_content.split(/\n/).map { |line| line.wrap(cols - 4) }.join("\n")
|
|
166
|
-
puts TTY::Box.frame("{bw}#{wrapped_content}{x}".c, width: cols)
|
|
167
|
-
rescue Errno::EPIPE
|
|
168
|
-
# Pipe closed, ignore
|
|
241
|
+
@results[:errors] += total
|
|
242
|
+
|
|
243
|
+
break unless Howzit.options[:force]
|
|
169
244
|
end
|
|
245
|
+
|
|
246
|
+
log_task_result(task, success)
|
|
170
247
|
end
|
|
248
|
+
output
|
|
249
|
+
end
|
|
171
250
|
|
|
251
|
+
##
|
|
252
|
+
## Run each subtopic in order, adding its results to this topic's
|
|
253
|
+
##
|
|
254
|
+
def run_subtopics(output)
|
|
255
|
+
@subtopics.each do |sub|
|
|
256
|
+
output.concat(sub.run(nested: true, as_subtopic: true))
|
|
257
|
+
%i[total success errors].each { |key| @results[key] += sub.results[key] }
|
|
258
|
+
break if halted?
|
|
259
|
+
end
|
|
172
260
|
output
|
|
173
261
|
end
|
|
174
262
|
|
|
@@ -178,7 +266,7 @@ module Howzit
|
|
|
178
266
|
end
|
|
179
267
|
|
|
180
268
|
def colored_option(color, topic, keys)
|
|
181
|
-
if topic.
|
|
269
|
+
if topic.all_tasks.empty?
|
|
182
270
|
''
|
|
183
271
|
else
|
|
184
272
|
optional = keys[:optional] =~ /[?!]+/ ? true : false
|
|
@@ -302,7 +390,8 @@ module Howzit
|
|
|
302
390
|
formatted_args = @arg_definitions.map { |arg| format_arg_definition(arg) }.join('{l}, '.c)
|
|
303
391
|
header_title += " {l}({x}#{formatted_args}{l}){x}".c
|
|
304
392
|
end
|
|
305
|
-
|
|
393
|
+
header_opts = opt[:subtopic] ? { color: '{bc}', hr: "\u{2508}" } : {}
|
|
394
|
+
output.push(header_title.format_header(header_opts))
|
|
306
395
|
output.push('')
|
|
307
396
|
end
|
|
308
397
|
# Process conditional blocks first
|
|
@@ -333,6 +422,8 @@ module Howzit
|
|
|
333
422
|
end
|
|
334
423
|
Howzit.named_arguments = @named_args
|
|
335
424
|
output.push('')
|
|
425
|
+
@subtopics.each { |sub| output.concat(sub.print_out(opt.merge(header: true, subtopic: true))) }
|
|
426
|
+
output
|
|
336
427
|
end
|
|
337
428
|
|
|
338
429
|
##
|
|
@@ -727,7 +818,7 @@ module Howzit
|
|
|
727
818
|
##
|
|
728
819
|
## Run directives sequentially with conditional re-evaluation
|
|
729
820
|
##
|
|
730
|
-
def run_sequential(
|
|
821
|
+
def run_sequential(output: [])
|
|
731
822
|
# Initialize conditional state
|
|
732
823
|
conditional_state = {} # { index => { evaluated: bool, result: bool, matched_chain: bool } }
|
|
733
824
|
directive_index = 0
|
|
@@ -737,16 +828,6 @@ module Howzit
|
|
|
737
828
|
Howzit.named_arguments ||= {}
|
|
738
829
|
Howzit.named_arguments.merge!(@named_args) if @named_args
|
|
739
830
|
|
|
740
|
-
unless @prereqs.empty?
|
|
741
|
-
begin
|
|
742
|
-
puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
|
|
743
|
-
rescue Errno::EPIPE
|
|
744
|
-
# Pipe closed, ignore
|
|
745
|
-
end
|
|
746
|
-
res = Prompt.yn('Have the above prerequisites been met?', default: true)
|
|
747
|
-
Process.exit 1 unless res
|
|
748
|
-
end
|
|
749
|
-
|
|
750
831
|
# Process directives sequentially
|
|
751
832
|
while directive_index < @directives.length
|
|
752
833
|
directive = @directives[directive_index]
|
|
@@ -905,30 +986,6 @@ module Howzit
|
|
|
905
986
|
re_evaluate_conditionals(conditional_state, directive_index - 1, context)
|
|
906
987
|
end
|
|
907
988
|
|
|
908
|
-
if @results[:total].positive?
|
|
909
|
-
total = "{bw}#{@results[:total]}{by} #{@results[:total] == 1 ? 'task' : 'tasks'}".c
|
|
910
|
-
errors = "{bw}#{@results[:errors]}{by} #{@results[:errors] == 1 ? 'error' : 'errors'}".c
|
|
911
|
-
@results[:message] += if @results[:errors].zero?
|
|
912
|
-
"{bg}\u{2713} {by}Ran #{total}{x}".c
|
|
913
|
-
elsif Howzit.options[:force]
|
|
914
|
-
"{br}\u{2715} {by}Completed #{total} with #{errors}{x}".c
|
|
915
|
-
else
|
|
916
|
-
"{br}\u{2715} {by}Ran #{total}, terminated due to error{x}".c
|
|
917
|
-
end
|
|
918
|
-
output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
|
|
919
|
-
end
|
|
920
|
-
|
|
921
|
-
unless @postreqs.empty?
|
|
922
|
-
begin
|
|
923
|
-
# Apply variable substitution to postreqs content, then wrap each line individually to preserve structure
|
|
924
|
-
postreqs_content = @postreqs.join("\n\n").render_arguments
|
|
925
|
-
wrapped_content = postreqs_content.split(/\n/).map { |line| line.wrap(cols - 4) }.join("\n")
|
|
926
|
-
puts TTY::Box.frame("{bw}#{wrapped_content}{x}".c, width: cols)
|
|
927
|
-
rescue Errno::EPIPE
|
|
928
|
-
# Pipe closed, ignore
|
|
929
|
-
end
|
|
930
|
-
end
|
|
931
|
-
|
|
932
989
|
output
|
|
933
990
|
end
|
|
934
991
|
|
data/lib/howzit/version.rb
CHANGED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
|
|
7
|
+
describe 'subtopics' do
|
|
8
|
+
let(:temp_dir) { File.expand_path(Dir.mktmpdir('howzit_subtopics_test')) }
|
|
9
|
+
let(:buildnote) { Howzit::BuildNote.new }
|
|
10
|
+
let(:parent) { buildnote.find_topic('Parent')[0] }
|
|
11
|
+
|
|
12
|
+
before do
|
|
13
|
+
File.write(
|
|
14
|
+
File.join(temp_dir, 'buildnotes.md'),
|
|
15
|
+
<<~NOTE
|
|
16
|
+
# Subtopic Project
|
|
17
|
+
|
|
18
|
+
## Parent
|
|
19
|
+
|
|
20
|
+
Parent content
|
|
21
|
+
|
|
22
|
+
@run(echo parent) Parent task
|
|
23
|
+
|
|
24
|
+
### Child One
|
|
25
|
+
|
|
26
|
+
@run(echo child1) Child one task
|
|
27
|
+
|
|
28
|
+
#### Grandchild
|
|
29
|
+
|
|
30
|
+
@run(echo grand) Grandchild task
|
|
31
|
+
|
|
32
|
+
### Child Two
|
|
33
|
+
|
|
34
|
+
@run(echo child2) Child two task
|
|
35
|
+
|
|
36
|
+
## Sibling
|
|
37
|
+
|
|
38
|
+
@run(echo sibling) Sibling task
|
|
39
|
+
NOTE
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
Dir.chdir(temp_dir)
|
|
43
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
44
|
+
Howzit.options[:stack] = false
|
|
45
|
+
Howzit.options[:include_upstream] = false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
after do
|
|
49
|
+
Dir.chdir(Dir.tmpdir)
|
|
50
|
+
FileUtils.rm_rf(temp_dir) if Dir.exist?(temp_dir)
|
|
51
|
+
Howzit.instance_variable_set(:@buildnote, nil)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def topic_named(title)
|
|
55
|
+
buildnote.topics.find { |t| t.title == title }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'nests deeper headers under the preceding shallower header' do
|
|
59
|
+
expect(parent.subtopics.map(&:title)).to eq(['Child One', 'Child Two'])
|
|
60
|
+
expect(topic_named('Child One').subtopics.map(&:title)).to eq(['Grandchild'])
|
|
61
|
+
expect(topic_named('Grandchild').ancestors.map(&:title)).to eq(['Child One', 'Parent'])
|
|
62
|
+
expect(topic_named('Sibling').parent_topic).to be_nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'keeps subtopics selectable on their own' do
|
|
66
|
+
expect(buildnote.find_topic('Child Two')[0].title).to eq('Child Two')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'collects tasks from subtopics in order' do
|
|
70
|
+
expect(parent.all_tasks.map(&:title)).to eq(['Parent task', 'Child one task', 'Grandchild task', 'Child two task'])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'displays subtopics with the parent topic' do
|
|
74
|
+
output = parent.print_out.join("\n").uncolor
|
|
75
|
+
|
|
76
|
+
expect(output).to include('Parent content')
|
|
77
|
+
expect(output).to include('Child One')
|
|
78
|
+
expect(output).to include('Grandchild')
|
|
79
|
+
expect(output).to include('echo child2')
|
|
80
|
+
expect(output).not_to include('Sibling')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it 'only outputs top-level topics when showing all topics' do
|
|
84
|
+
expect(buildnote.top_level_topics.map(&:title)).to eq(%w[Parent Sibling])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'indents subtopics in the topic list' do
|
|
88
|
+
list = buildnote.list.uncolor
|
|
89
|
+
|
|
90
|
+
expect(list).to include("\n- Parent")
|
|
91
|
+
expect(list).to include("\n - Child One")
|
|
92
|
+
expect(list).to include("\n - Grandchild")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe 'running a parent topic' do
|
|
96
|
+
let(:ran) { [] }
|
|
97
|
+
|
|
98
|
+
it 'runs its own tasks followed by all subtopic tasks' do
|
|
99
|
+
ran_titles = ran
|
|
100
|
+
allow_any_instance_of(Howzit::Task).to receive(:run) do |task|
|
|
101
|
+
ran_titles << task.title
|
|
102
|
+
[[], 1, true]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
parent.run
|
|
106
|
+
|
|
107
|
+
expect(ran).to eq(['Parent task', 'Child one task', 'Grandchild task', 'Child two task'])
|
|
108
|
+
expect(parent.results[:total]).to eq(4)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'stops running subtopics after an error' do
|
|
112
|
+
ran_titles = ran
|
|
113
|
+
allow_any_instance_of(Howzit::Task).to receive(:run) do |task|
|
|
114
|
+
ran_titles << task.title
|
|
115
|
+
[[], 1, task.title != 'Child one task']
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
parent.run
|
|
119
|
+
|
|
120
|
+
expect(ran).to eq(['Parent task', 'Child one task'])
|
|
121
|
+
expect(parent.results[:errors]).to eq(1)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'continues past errors with --force' do
|
|
125
|
+
Howzit.options[:force] = true
|
|
126
|
+
ran_titles = ran
|
|
127
|
+
allow_any_instance_of(Howzit::Task).to receive(:run) do |task|
|
|
128
|
+
ran_titles << task.title
|
|
129
|
+
[[], 1, task.title != 'Child one task']
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
parent.run
|
|
133
|
+
|
|
134
|
+
expect(ran).to eq(['Parent task', 'Child one task', 'Grandchild task', 'Child two task'])
|
|
135
|
+
ensure
|
|
136
|
+
Howzit.options[:force] = false
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
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.44
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brett Terpstra
|
|
@@ -13,14 +13,14 @@ dependencies:
|
|
|
13
13
|
name: bundler
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
18
|
version: '2.2'
|
|
19
19
|
type: :development
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '2.2'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
@@ -331,6 +331,7 @@ files:
|
|
|
331
331
|
- spec/spec_helper.rb
|
|
332
332
|
- spec/stack_mode_spec.rb
|
|
333
333
|
- spec/stringutils_spec.rb
|
|
334
|
+
- spec/subtopics_spec.rb
|
|
334
335
|
- spec/task_spec.rb
|
|
335
336
|
- spec/topic_positional_snapshot_spec.rb
|
|
336
337
|
- spec/topic_spec.rb
|
|
@@ -356,7 +357,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
356
357
|
- !ruby/object:Gem::Version
|
|
357
358
|
version: '0'
|
|
358
359
|
requirements: []
|
|
359
|
-
rubygems_version:
|
|
360
|
+
rubygems_version: 4.0.18
|
|
360
361
|
specification_version: 4
|
|
361
362
|
summary: Provides a way to access Markdown project notes by topic with query capabilities
|
|
362
363
|
and the ability to execute the tasks it describes.
|
|
@@ -377,6 +378,7 @@ test_files:
|
|
|
377
378
|
- spec/spec_helper.rb
|
|
378
379
|
- spec/stack_mode_spec.rb
|
|
379
380
|
- spec/stringutils_spec.rb
|
|
381
|
+
- spec/subtopics_spec.rb
|
|
380
382
|
- spec/task_spec.rb
|
|
381
383
|
- spec/topic_positional_snapshot_spec.rb
|
|
382
384
|
- spec/topic_spec.rb
|