doing 2.1.10 → 2.1.11

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.
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: