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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8916b2ac37cd920db8008fe7a9aa28a80b4c5265083b4b257fb8ea1113938efb
4
- data.tar.gz: 9486d6ca52f82ea5059bffd0de58333b82eedfc623d03f6325817caf8c956d4f
3
+ metadata.gz: 881188642b99f4be83a7697440251ec8890e6d847468c3984a72183939196857
4
+ data.tar.gz: c622b23318a075aa277a0a197e1ec93d8023e52e99018557e7eb6f8309c76927
5
5
  SHA512:
6
- metadata.gz: 5819786618c45538e42f21243102043b1ed408f516b0b946adaa9a2819d0080c2804cba47987f4ba0ed98b21ad7499d42cc825c2f89efb63cc4f6d8013bd743d
7
- data.tar.gz: 2a90edf5e41602e1c6fc50ab21a6bfc3531b598f3a5cc381492f284bbb588b2590589fbc23da0f007a021605d46f7246e2908eed89895c175c24838b4755b285
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
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rspec, rubocop plugins, and the .rubocop_todo.yml cops depend on the locked bundle
4
+ exec('bundle', 'exec', 'rake', *ARGV) unless ENV['BUNDLE_GEMFILE']
5
+
3
6
  # require 'bump/tasks'
4
7
  require 'English'
5
8
  require 'bundler/gem_tasks'
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', '~> 2.2'
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'
@@ -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.tasks.count.positive?
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
- s_out = []
226
-
227
- topic.tasks.each { |task| s_out.push(task.to_list) }
251
+ next if topic.all_tasks.empty?
228
252
 
229
- next if s_out.empty?
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
- split = help.split(/^##+/)
960
- split.slice!(0)
961
- split.each do |sect|
962
- next if sect.strip.empty?
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, source_file: source_file)
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 = topics.length > 1
1377
- topics.each { |k| output.push(process_topic(k, false, single: false)) }
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
- topics.each { |k| output.push(process_topic(k, false, single: false)) }
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
- topic_matches.each { |topic_match| output.push(process_topic(topic_match, Howzit.options[:run], single: true)) }
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
- topics.each { |k| output.push(process_topic(k, false, single: false)) }
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].tasks.count} tasks{x}".c)
155
+ Howzit.console.info("{by}End include: #{matches[0].all_tasks.count} tasks{x}".c)
156
156
  @last_status = nil
157
- [output, matches[0].tasks.count]
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].tasks.count
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
- # Handle run command, execute directives in topic
99
- def run(nested: false)
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
- # Use sequential processing if we have directives with conditionals
105
- if @directives && @directives.any?(&:conditional?)
106
- return run_sequential(nested: nested, output: output, cols: cols)
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
- # Fall back to old behavior for backward compatibility
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 @tasks.count.positive?
114
- unless @prereqs.empty?
115
- begin
116
- puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols)
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
- end
167
+ show_postreqs(cols)
124
168
 
125
- @tasks.each do |task|
126
- next if (task.optional || Howzit.options[:ask]) && !ask_task(task)
169
+ output
170
+ end
127
171
 
128
- run_output, total, success = task.run
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
- output.concat(run_output)
131
- @results[:total] += total
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
- if success
134
- @results[:success] += total
135
- else
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
- @results[:errors] += total
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
- break unless Howzit.options[:force]
141
- end
201
+ def show_postreqs(cols)
202
+ return if @postreqs.empty?
142
203
 
143
- log_task_result(task, success)
144
- end
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
- total = "{bw}#{@results[:total]}{by} #{@results[:total] == 1 ? 'task' : 'tasks'}".c
147
- errors = "{bw}#{@results[:errors]}{by} #{@results[:errors] == 1 ? 'error' : 'errors'}".c
148
- @results[:message] += if @results[:errors].zero?
149
- "{bg}\u{2713} {by}Ran #{total}{x}".c
150
- elsif Howzit.options[:force]
151
- "{br}\u{2715} {by}Completed #{total} with #{errors}{x}".c
152
- else
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
- Howzit.console.warn "{r}--run: No {br}@directive{xr} found in {bw}#{@title}{x}".c
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
- output.push(@results[:message]) if Howzit.options[:log_level] < 2 && !nested && !Howzit.options[:run]
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
- unless @postreqs.empty?
162
- begin
163
- # Apply variable substitution to postreqs content, then wrap each line individually to preserve structure
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.tasks.empty?
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
- output.push(header_title.format_header)
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(nested: false, output: [], cols: 80)
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
 
@@ -3,5 +3,5 @@
3
3
  # Primary module for this gem.
4
4
  module Howzit
5
5
  # Current Howzit version.
6
- VERSION = '2.1.42'
6
+ VERSION = '2.1.44'
7
7
  end
@@ -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.42
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: 3.6.7
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