doing 2.1.10 → 2.1.11

Sign up to get free protection for your applications and to get access to all the features.
data/lib/doing/wwid.rb CHANGED
@@ -114,6 +114,8 @@ module Doing
114
114
  filename = @doing_file if filename.nil?
115
115
  return if File.exist?(filename) && File.stat(filename).size.positive?
116
116
 
117
+ FileUtils.mkdir_p(File.dirname(filename)) unless File.directory?(File.dirname(filename))
118
+
117
119
  File.open(filename, 'w+') do |f|
118
120
  f.puts "#{@config['current_section']}:"
119
121
  end
@@ -372,9 +374,13 @@ module Doing
372
374
  end
373
375
  end
374
376
 
377
+ Hooks.trigger :pre_entry_add, self, entry
378
+
375
379
  @content.push(entry)
376
380
  # logger.count(:added, level: :debug)
377
381
  logger.info('New entry:', %(added "#{entry.title}" to #{section}))
382
+
383
+ Hooks.trigger :post_entry_added, self, entry.dup
378
384
  end
379
385
 
380
386
  ##
@@ -471,6 +477,7 @@ module Doing
471
477
  else
472
478
  item.title.tag!('done')
473
479
  end
480
+ Hooks.trigger :post_entry_updated, self, item
474
481
  end
475
482
 
476
483
  # Remove @done tag
@@ -798,6 +805,65 @@ module Doing
798
805
  output
799
806
  end
800
807
 
808
+ def delete_items(items, force: false)
809
+ res = force ? true : Prompt.yn("Delete #{items.size} #{items.size == 1 ? 'item' : 'items'}?", default_response: 'y')
810
+ if res
811
+ items.each do |i|
812
+ deleted = @content.delete_item(i, single: items.count == 1)
813
+ Hooks.trigger :post_entry_removed, self, deleted
814
+ end
815
+ write(@doing_file)
816
+ end
817
+ end
818
+
819
+ def edit_items(items)
820
+ editable_items = []
821
+
822
+ items.each do |i|
823
+ editable = "#{i.date.strftime('%F %R')} | #{i.title}"
824
+ old_note = i.note ? i.note.strip_lines.join("\n") : nil
825
+ editable += "\n#{old_note}" unless old_note.nil?
826
+ editable_items << editable
827
+ end
828
+ divider = "\n-----------\n"
829
+ notice =<<~EONOTICE
830
+ # - You may delete entries, but leave all divider lines (---) in place.
831
+ # - Start and @done dates replaced with a time string (yesterday 3pm) will
832
+ # be parsed automatically. Do not delete the pipe (|) between start date
833
+ # and entry title.
834
+ EONOTICE
835
+ input = "#{editable_items.map(&:strip).join(divider)}\n\n#{notice}"
836
+
837
+ new_items = fork_editor(input).split(/#{divider}/)
838
+
839
+ new_items.each_with_index do |new_item, i|
840
+ input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
841
+ first_line = input_lines[0]&.strip
842
+
843
+ if first_line.nil? || first_line =~ /^#{divider.strip}$/ || first_line.strip.empty?
844
+ deleted = @content.delete_item(items[i], single: new_items.count == 1)
845
+ Hooks.trigger :post_entry_removed, self, deleted
846
+ Doing.logger.count(:deleted)
847
+ else
848
+ date, title, note = format_input(new_item)
849
+
850
+ note.map!(&:strip)
851
+ note.delete_if(&:ignore?)
852
+ item = items[i]
853
+ old_item = item.dup
854
+ item.date = date || items[i].date
855
+ item.title = title
856
+ item.note = note
857
+ if (item.equal?(old_item))
858
+ Doing.logger.count(:skipped, level: :debug)
859
+ else
860
+ Doing.logger.count(:updated)
861
+ Hooks.trigger :post_entry_updated, self, item
862
+ end
863
+ end
864
+ end
865
+ end
866
+
801
867
  ##
802
868
  ## Display an interactive menu of entries
803
869
  ##
@@ -958,7 +1024,7 @@ module Doing
958
1024
 
959
1025
  item = items[0]
960
1026
  if opt[:resume] && !opt[:reset]
961
- repeat_item(item, { editor: opt[:editor] })
1027
+ repeat_item(item, { editor: opt[:editor] }) # hooked
962
1028
  elsif opt[:reset]
963
1029
  res = Prompt.enter_text('Start date (blank for current time)', default_response: '')
964
1030
  if res =~ /^ *$/
@@ -972,7 +1038,9 @@ module Doing
972
1038
  else
973
1039
  opt[:resume]
974
1040
  end
975
- @content.update_item(item, reset_item(item, date: date, resume: res))
1041
+ new_entry = reset_item(item, date: date, resume: res)
1042
+ @content.update_item(item, new_entry)
1043
+ Hooks.trigger :post_entry_updated, self, new_entry
976
1044
  end
977
1045
  write(@doing_file)
978
1046
 
@@ -980,11 +1048,7 @@ module Doing
980
1048
  end
981
1049
 
982
1050
  if opt[:delete]
983
- res = opt[:force] ? true : Prompt.yn("Delete #{items.size} items?", default_response: 'y')
984
- if res
985
- items.each { |i| @content.delete_item(i, single: items.count == 1) }
986
- write(@doing_file)
987
- end
1051
+ delete_items(items, force: opt[:force]) # hooked
988
1052
  return
989
1053
  end
990
1054
 
@@ -992,6 +1056,7 @@ module Doing
992
1056
  tag = @config['marker_tag'] || 'flagged'
993
1057
  items.map! do |i|
994
1058
  i.tag(tag, date: false, remove: opt[:remove], single: single)
1059
+ Hooks.trigger :post_entry_updated, self, i
995
1060
  end
996
1061
  end
997
1062
 
@@ -1001,6 +1066,7 @@ module Doing
1001
1066
  if i.should_finish?
1002
1067
  should_date = !opt[:cancel] && i.should_time?
1003
1068
  i.tag(tag, date: should_date, remove: opt[:remove], single: single)
1069
+ Hooks.trigger :post_entry_updated, self, i
1004
1070
  end
1005
1071
  end
1006
1072
  end
@@ -1009,61 +1075,22 @@ module Doing
1009
1075
  tag = opt[:tag]
1010
1076
  items.map! do |i|
1011
1077
  i.tag(tag, date: false, remove: opt[:remove], single: single)
1078
+ Hooks.trigger :post_entry_updated, self, i
1012
1079
  end
1013
1080
  end
1014
1081
 
1015
1082
  if opt[:archive] || opt[:move]
1016
1083
  section = opt[:archive] ? 'Archive' : guess_section(opt[:move])
1017
- items.map! { |i| i.move_to(section, label: true) }
1084
+ items.map! do |i|
1085
+ i.move_to(section, label: true)
1086
+ Hooks.trigger :post_entry_updated, self, i
1087
+ end
1018
1088
  end
1019
1089
 
1020
1090
  write(@doing_file)
1021
1091
 
1022
1092
  if opt[:editor]
1023
-
1024
- editable_items = []
1025
-
1026
- items.each do |i|
1027
- editable = "#{i.date.strftime('%F %R')} | #{i.title}"
1028
- old_note = i.note ? i.note.strip_lines.join("\n") : nil
1029
- editable += "\n#{old_note}" unless old_note.nil?
1030
- editable_items << editable
1031
- end
1032
- divider = "\n-----------\n"
1033
- notice =<<~EONOTICE
1034
- # - You may delete entries, but leave all divider lines (---) in place.
1035
- # - Start and @done dates replaced with a time string (yesterday 3pm) will
1036
- # be parsed automatically. Do not delete the pipe (|) between start date
1037
- # and entry title.
1038
- EONOTICE
1039
- input = "#{editable_items.map(&:strip).join(divider)}\n\n#{notice}"
1040
-
1041
- new_items = fork_editor(input).split(/#{divider}/)
1042
-
1043
- new_items.each_with_index do |new_item, i|
1044
- input_lines = new_item.split(/[\n\r]+/).delete_if(&:ignore?)
1045
- first_line = input_lines[0]&.strip
1046
-
1047
- if first_line.nil? || first_line =~ /^#{divider.strip}$/ || first_line.strip.empty?
1048
- @content.delete_item(items[i], single: new_items.count == 1)
1049
- Doing.logger.count(:deleted)
1050
- else
1051
- date, title, note = format_input(new_item)
1052
-
1053
- note.map!(&:strip)
1054
- note.delete_if(&:ignore?)
1055
- item = items[i]
1056
- old_item = item.dup
1057
- item.date = date || items[i].date
1058
- item.title = title
1059
- item.note = note
1060
- if (item.equal?(old_item))
1061
- Doing.logger.count(:skipped, level: :debug)
1062
- else
1063
- Doing.logger.count(:updated)
1064
- end
1065
- end
1066
- end
1093
+ edit_items(items) # hooked
1067
1094
 
1068
1095
  write(@doing_file)
1069
1096
  end
@@ -1088,7 +1115,7 @@ module Doing
1088
1115
  options[:template] = opt[:template] || nil
1089
1116
  end
1090
1117
 
1091
- output = list_section(options)
1118
+ output = list_section(options) # hooked
1092
1119
 
1093
1120
  if opt[:save_to]
1094
1121
  file = File.expand_path(opt[:save_to])
@@ -1116,7 +1143,7 @@ module Doing
1116
1143
  ##
1117
1144
  ## @see #filter_items
1118
1145
  ##
1119
- def tag_last(opt)
1146
+ def tag_last(opt) # hooked
1120
1147
  opt ||= {}
1121
1148
  opt[:count] ||= 1
1122
1149
  opt[:archive] ||= false
@@ -1227,6 +1254,8 @@ module Doing
1227
1254
  elsif opt[:archive] && opt[:count].zero?
1228
1255
  logger.warn('Skipped:', 'Archiving is skipped when operating on all entries')
1229
1256
  end
1257
+
1258
+ Hooks.trigger :post_entry_updated, self, item
1230
1259
  end
1231
1260
 
1232
1261
  write(@doing_file)
@@ -1280,6 +1309,7 @@ module Doing
1280
1309
  item.title = title
1281
1310
  item.note.add(note, replace: true)
1282
1311
  logger.info('Edited:', item.title)
1312
+ Hooks.trigger :post_entry_updated, self, item.dup
1283
1313
 
1284
1314
  write(@doing_file)
1285
1315
  end
@@ -1334,8 +1364,10 @@ module Doing
1334
1364
  logger.count(:completed)
1335
1365
  logger.info('Completed:', item.title)
1336
1366
  end
1367
+ Hooks.trigger :post_entry_updated, self, item
1337
1368
  end
1338
1369
 
1370
+
1339
1371
  logger.debug('Skipped:', "No active @#{tag} tasks found.") if found_items.zero?
1340
1372
 
1341
1373
  if opt[:new_item]
@@ -1393,6 +1425,7 @@ module Doing
1393
1425
 
1394
1426
  unless ((!tags.empty? && !item.tags?(tags, bool)) || (opt[:search] && !item.search(opt[:search].to_s)) || (opt[:before] && item.date >= cutoff))
1395
1427
  new_item = @content.delete(item)
1428
+ Hooks.trigger :post_entry_removed, self, item.dup
1396
1429
  raise DoingRuntimeError, "Error deleting item: #{item}" if new_item.nil?
1397
1430
 
1398
1431
  new_content.add_section(new_item.section, log: false)
@@ -1503,7 +1536,7 @@ module Doing
1503
1536
  tpl_cfg = @config.dig('templates', opt[:config_template])
1504
1537
 
1505
1538
  cfg = if opt[:view_template]
1506
- @config.dig('views', opt[:view_template]).deep_merge(tpl_cfg)
1539
+ @config.dig('views', opt[:view_template]).deep_merge(tpl_cfg, { extend_existing_arrays: true, sort_merged_arrays: true })
1507
1540
  else
1508
1541
  tpl_cfg
1509
1542
  end
@@ -1515,7 +1548,7 @@ module Doing
1515
1548
  'tags_color' => @config['tags_color'],
1516
1549
  'duration' => @config['duration'],
1517
1550
  'interval_format' => @config['interval_format']
1518
- })
1551
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
1519
1552
  opt[:duration] ||= cfg['duration'] || false
1520
1553
  opt[:interval_format] ||= cfg['interval_format'] || 'text'
1521
1554
  opt[:count] ||= 0
@@ -1551,7 +1584,13 @@ module Doing
1551
1584
 
1552
1585
  items.reverse! unless opt[:order] =~ /^d/i
1553
1586
 
1554
- if opt[:interactive]
1587
+ if opt[:delete]
1588
+ delete_items(items, force: opt[:force])
1589
+ return
1590
+ elsif opt[:editor]
1591
+ edit_items(items)
1592
+ return
1593
+ elsif opt[:interactive]
1555
1594
  opt[:menu] = !opt[:force]
1556
1595
  opt[:query] = '' # opt[:search]
1557
1596
  opt[:multiple] = true
@@ -1611,14 +1650,14 @@ module Doing
1611
1650
  opt[:totals] ||= false
1612
1651
  opt[:sort_tags] ||= false
1613
1652
 
1614
- cfg = @config['templates']['today'].deep_merge(@config['templates']['default']).deep_merge({
1653
+ cfg = @config['templates']['today'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
1615
1654
  'wrap_width' => @config['wrap_width'] || 0,
1616
1655
  'date_format' => @config['default_date_format'],
1617
1656
  'order' => @config['order'] || 'asc',
1618
1657
  'tags_color' => @config['tags_color'],
1619
1658
  'duration' => @config['duration'],
1620
1659
  'interval_format' => @config['interval_format']
1621
- })
1660
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
1622
1661
 
1623
1662
  opt[:duration] ||= cfg['duration'] || false
1624
1663
  opt[:interval_format] ||= cfg['interval_format'] || 'text'
@@ -1727,14 +1766,14 @@ module Doing
1727
1766
  opt[:totals] ||= false
1728
1767
  opt[:sort_tags] ||= false
1729
1768
 
1730
- cfg = @config['templates']['recent'].deep_merge(@config['templates']['default']).deep_merge({
1769
+ cfg = @config['templates']['recent'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
1731
1770
  'wrap_width' => @config['wrap_width'] || 0,
1732
1771
  'date_format' => @config['default_date_format'],
1733
1772
  'order' => @config['order'] || 'asc',
1734
1773
  'tags_color' => @config['tags_color'],
1735
1774
  'duration' => @config['duration'],
1736
1775
  'interval_format' => @config['interval_format']
1737
- })
1776
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
1738
1777
  opt[:duration] ||= cfg['duration'] || false
1739
1778
  opt[:interval_format] ||= cfg['interval_format'] || 'text'
1740
1779
 
@@ -1761,14 +1800,14 @@ module Doing
1761
1800
  ##
1762
1801
  def last(times: true, section: nil, options: {})
1763
1802
  section = section.nil? || section =~ /all/i ? 'All' : guess_section(section)
1764
- cfg = @config['templates']['last'].deep_merge(@config['templates']['default']).deep_merge({
1803
+ cfg = @config['templates']['last'].deep_merge(@config['templates']['default'], { extend_existing_arrays: true, sort_merged_arrays: true }).deep_merge({
1765
1804
  'wrap_width' => @config['wrap_width'] || 0,
1766
1805
  'date_format' => @config['default_date_format'],
1767
1806
  'order' => @config['order'] || 'asc',
1768
1807
  'tags_color' => @config['tags_color'],
1769
1808
  'duration' => @config['duration'],
1770
1809
  'interval_format' => @config['interval_format']
1771
- })
1810
+ }, { extend_existing_arrays: true, sort_merged_arrays: true })
1772
1811
  options[:duration] ||= cfg['duration'] || false
1773
1812
  options[:interval_format] ||= cfg['interval_format'] || 'text'
1774
1813
 
@@ -1783,7 +1822,8 @@ module Doing
1783
1822
  interval_format: options[:interval_format],
1784
1823
  case: options[:case],
1785
1824
  not: options[:negate],
1786
- config_template: 'last'
1825
+ config_template: 'last',
1826
+ delete: options[:delete]
1787
1827
  }
1788
1828
 
1789
1829
  if options[:tag]
@@ -1832,6 +1872,7 @@ module Doing
1832
1872
 
1833
1873
  @config['autotag']['synonyms'].each do |tag, v|
1834
1874
  v.each do |word|
1875
+ word = word.wildcard_to_rx
1835
1876
  next unless text =~ /\b#{word}\b/i
1836
1877
 
1837
1878
  unless current_tags.include?(tag) || tagged[:whitelisted].include?(tag)
@@ -1845,7 +1886,12 @@ module Doing
1845
1886
  @config['autotag']['transform'].each do |tag|
1846
1887
  next unless tag =~ /\S+:\S+/
1847
1888
 
1848
- rx, r = tag.split(/:/)
1889
+ if tag =~ /::/
1890
+ rx, r = tag.split(/::/)
1891
+ else
1892
+ rx, r = tag.split(/:/)
1893
+ end
1894
+
1849
1895
  flag_rx = %r{/([r]+)$}
1850
1896
  if r =~ flag_rx
1851
1897
  flags = r.match(flag_rx)[1].split(//)
@@ -2087,6 +2133,36 @@ EOS
2087
2133
  end
2088
2134
  end
2089
2135
 
2136
+ def configure(filename = nil)
2137
+ if filename
2138
+ Doing.config_with(filename, { ignore_local: true })
2139
+ elsif ENV['DOING_CONFIG']
2140
+ Doing.config_with(ENV['DOING_CONFIG'], { ignore_local: true })
2141
+ end
2142
+
2143
+ Doing.logger.benchmark(:configure, :start)
2144
+ config = Doing.config
2145
+ Doing.logger.benchmark(:configure, :finish)
2146
+
2147
+ config.settings['backup_dir'] = ENV['DOING_BACKUP_DIR'] if ENV['DOING_BACKUP_DIR']
2148
+ @config = config.settings
2149
+ end
2150
+
2151
+ def get_diff(filename = nil)
2152
+ configure if @config.nil?
2153
+
2154
+ filename ||= @config['doing_file']
2155
+ init_doing_file(filename)
2156
+ current_content = @content.dup
2157
+ backup_file = Util::Backup.last_backup(filename, count: 1)
2158
+ raise DoingRuntimeError, 'No undo history to diff' if backup_file.nil?
2159
+
2160
+ backup = WWID.new
2161
+ backup.config = @config
2162
+ backup.init_doing_file(backup_file)
2163
+ current_content.diff(backup.content)
2164
+ end
2165
+
2090
2166
  private
2091
2167
 
2092
2168
  ##
@@ -2127,6 +2203,8 @@ EOS
2127
2203
 
2128
2204
  export_options = { page_title: title, is_single: is_single, options: opt }
2129
2205
 
2206
+ Hooks.trigger :pre_export, self, opt[:output], items
2207
+
2130
2208
  Plugins.plugins[:export].each do |_, options|
2131
2209
  next unless opt[:output] =~ /^(#{options[:trigger].normalize_trigger})$/i
2132
2210
 
@@ -2176,10 +2254,11 @@ EOS
2176
2254
 
2177
2255
  section_items = @content.in_section(section)
2178
2256
  max = section_items.count - count.to_i
2257
+ moved_items = []
2179
2258
 
2180
2259
  counter = 0
2181
2260
 
2182
- @content.map! do |item|
2261
+ @content.map do |item|
2183
2262
  break if counter >= max
2184
2263
  if opt[:before]
2185
2264
  time_string = opt[:before]
@@ -2193,6 +2272,8 @@ EOS
2193
2272
  else
2194
2273
  counter += 1
2195
2274
  item.move_to(destination, label: label, log: false)
2275
+ Hooks.trigger :post_entry_updated, self, item.dup
2276
+ item
2196
2277
  end
2197
2278
  end
2198
2279
 
data/lib/doing.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'doing/version'
3
4
  require 'time'
4
5
  require 'date'
@@ -18,5 +18,36 @@ module Doing
18
18
  Hooks.register :post_write do |filename|
19
19
  res = `/bin/bash /Users/ttscoff/scripts/after_doing.sh`.strip
20
20
  Doing.logger.debug('Hooks:', res) unless res =~ /^\.\.\.$/
21
+
22
+ wwid = WWID.new
23
+ wwid.configure
24
+ if filename == wwid.config['doing_file']
25
+ diff = wwid.get_diff(filename)
26
+ puts diff
27
+ end
28
+ end
29
+
30
+ Hooks.register :post_entry_added do |wwid, entry|
31
+ if wwid.config.key?('day_one_trigger') && entry.tags?(wwid.config['day_one_trigger'], :and)
32
+
33
+ logger.info('New entry:', 'Adding to Day One')
34
+ add_to_day_one(entry, wwid.config)
35
+ end
36
+ end
37
+
38
+ ##
39
+ ## Add the entry to Day One using the CLI
40
+ ##
41
+ ## @param entry The entry to add
42
+ ##
43
+ def self.add_to_day_one(entry, config)
44
+ dayone = TTY::Which.which('dayone2')
45
+ flagged = entry.tags?('flagged') ? ' -s' : ''
46
+ tags = entry.tags.map { |t| Shellwords.escape(t) }.join(' ')
47
+ tags = tags.length.positive? ? " -t #{tags}" : ''
48
+ date = " -d '#{entry.date.strftime('%Y-%m-%d %H:%M:%S')}'"
49
+ title = entry.title.tag(config['day_one_trigger'], remove: true)
50
+ title += "\n#{entry.note}" unless entry.note.empty?
51
+ `echo #{Shellwords.escape(title)} | #{dayone} new#{flagged}#{date}#{tags}`
21
52
  end
22
53
  end
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env ruby
2
+ require 'awesome_print'
3
+
4
+ input = STDIN.read.force_encoding('utf-8')
5
+
6
+ commands = input.split(/^# @@/).delete_if(&:empty?).sort
7
+ # commands.each do |cmd|
8
+ # puts cmd.split(/^(\w+)(.*)$/m)[1]
9
+ # end
10
+ indexes = %w[
11
+ again
12
+ cancel
13
+ done
14
+ finish
15
+ later
16
+ mark
17
+ meanwhile
18
+ note
19
+ now
20
+ reset
21
+ select
22
+ tag
23
+ choose
24
+ grep
25
+ last
26
+ recent
27
+ show
28
+ tags
29
+ today
30
+ view
31
+ yesterday
32
+ open
33
+ config
34
+ archive
35
+ import
36
+ rotate
37
+ colors
38
+ completion
39
+ plugins
40
+ sections
41
+ template
42
+ views
43
+ undo
44
+ redo
45
+ add_section
46
+ tag_dir
47
+ changelog
48
+ ]
49
+
50
+ result = Array.new(indexes.count)
51
+
52
+ commands.each do |cmd|
53
+ key = cmd.match(/^(\w+)/)[1]
54
+ idx = indexes.index(key)
55
+ result[idx] = "#@@#{cmd}"
56
+ # puts commands.join('# @@')
57
+ end
58
+
59
+ puts result.join('')
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: doing
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.10
4
+ version: 2.1.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-21 00:00:00.000000000 Z
11
+ date: 2022-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: safe_yaml
@@ -405,6 +405,10 @@ files:
405
405
  - AUTHORS
406
406
  - CHANGELOG.md
407
407
  - COMMANDS.md
408
+ - Dockerfile
409
+ - Dockerfile-2.6
410
+ - Dockerfile-2.7
411
+ - Dockerfile-3.0
408
412
  - Gemfile
409
413
  - Gemfile.lock
410
414
  - LICENSE
@@ -642,6 +646,7 @@ files:
642
646
  - scripts/generate_bash_completions.rb
643
647
  - scripts/generate_fish_completions.rb
644
648
  - scripts/generate_zsh_completions.rb
649
+ - scripts/sort_commands.rb
645
650
  - yard_templates/default/method_details/setup.rb
646
651
  homepage: http://brettterpstra.com/project/doing/
647
652
  licenses: