doing 2.1.1pre → 2.1.5pre

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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +58 -8
  6. data/Gemfile.lock +25 -1
  7. data/README.md +1 -1
  8. data/Rakefile +2 -0
  9. data/bin/doing +447 -149
  10. data/doc/Array.html +63 -1
  11. data/doc/BooleanTermParser/Clause.html +293 -0
  12. data/doc/BooleanTermParser/Operator.html +172 -0
  13. data/doc/BooleanTermParser/Query.html +417 -0
  14. data/doc/BooleanTermParser/QueryParser.html +135 -0
  15. data/doc/BooleanTermParser/QueryTransformer.html +124 -0
  16. data/doc/BooleanTermParser.html +115 -0
  17. data/doc/Doing/CLIFormat.html +131 -0
  18. data/doc/Doing/Color.html +2 -2
  19. data/doc/Doing/Completion.html +1 -1
  20. data/doc/Doing/Configuration.html +168 -73
  21. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  22. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  23. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  24. data/doc/Doing/Errors/EmptyInput.html +1 -1
  25. data/doc/Doing/Errors/NoResults.html +1 -1
  26. data/doc/Doing/Errors/PluginException.html +1 -1
  27. data/doc/Doing/Errors/UserCancelled.html +1 -1
  28. data/doc/Doing/Errors/WrongCommand.html +1 -1
  29. data/doc/Doing/Errors.html +1 -1
  30. data/doc/Doing/Hooks.html +1 -1
  31. data/doc/Doing/Item.html +177 -86
  32. data/doc/Doing/Items.html +36 -2
  33. data/doc/Doing/LogAdapter.html +70 -1
  34. data/doc/Doing/Note.html +5 -134
  35. data/doc/Doing/Pager.html +1 -1
  36. data/doc/Doing/Plugins.html +380 -40
  37. data/doc/Doing/Prompt.html +70 -18
  38. data/doc/Doing/Section.html +1 -1
  39. data/doc/Doing/TemplateString.html +713 -0
  40. data/doc/Doing/Util/Backup.html +686 -0
  41. data/doc/Doing/Util.html +16 -4
  42. data/doc/Doing/WWID.html +133 -73
  43. data/doc/Doing.html +4 -4
  44. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  45. data/doc/GLI/Commands.html +1 -1
  46. data/doc/GLI.html +1 -1
  47. data/doc/Hash.html +1 -1
  48. data/doc/PhraseParser/Operator.html +172 -0
  49. data/doc/PhraseParser/PhraseClause.html +303 -0
  50. data/doc/PhraseParser/Query.html +495 -0
  51. data/doc/PhraseParser/QueryParser.html +136 -0
  52. data/doc/PhraseParser/QueryTransformer.html +124 -0
  53. data/doc/PhraseParser/TermClause.html +293 -0
  54. data/doc/PhraseParser.html +115 -0
  55. data/doc/Status.html +1 -1
  56. data/doc/String.html +319 -13
  57. data/doc/Symbol.html +35 -1
  58. data/doc/Time.html +70 -2
  59. data/doc/_index.html +132 -4
  60. data/doc/class_list.html +1 -1
  61. data/doc/file.README.html +2 -2
  62. data/doc/index.html +2 -2
  63. data/doc/method_list.html +648 -160
  64. data/doc/top-level-namespace.html +2 -2
  65. data/doing.gemspec +3 -0
  66. data/doing.rdoc +263 -82
  67. data/lib/completion/doing.bash +18 -18
  68. data/lib/doing/array.rb +9 -0
  69. data/lib/doing/boolean_term_parser.rb +86 -0
  70. data/lib/doing/configuration.rb +63 -24
  71. data/lib/doing/item.rb +112 -10
  72. data/lib/doing/items.rb +6 -0
  73. data/lib/doing/log_adapter.rb +28 -0
  74. data/lib/doing/note.rb +31 -30
  75. data/lib/doing/phrase_parser.rb +124 -0
  76. data/lib/doing/plugin_manager.rb +57 -13
  77. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  78. data/lib/doing/plugins/export/template_export.rb +113 -81
  79. data/lib/doing/prompt.rb +26 -13
  80. data/lib/doing/string.rb +114 -29
  81. data/lib/doing/string_chronify.rb +5 -1
  82. data/lib/doing/symbol.rb +4 -0
  83. data/lib/doing/template_string.rb +197 -0
  84. data/lib/doing/time.rb +32 -0
  85. data/lib/doing/util.rb +6 -7
  86. data/lib/doing/util_backup.rb +287 -0
  87. data/lib/doing/version.rb +1 -1
  88. data/lib/doing/wwid.rb +152 -55
  89. data/lib/doing.rb +9 -0
  90. data/lib/templates/doing-dayone-entry.erb +6 -0
  91. data/lib/templates/doing-dayone.erb +5 -0
  92. metadata +85 -2
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## Template string formatting
6
+ ##
7
+ class TemplateString < String
8
+ class ::String
9
+ ##
10
+ ## Extract the longest valid color from a string.
11
+ ##
12
+ ## Allows %colors to bleed into other text and still
13
+ ## be recognized, e.g. %greensomething still finds
14
+ ## %green.
15
+ ##
16
+ ## @return [String] a valid color name
17
+ ## @api private
18
+ def validate_color
19
+ valid_color = nil
20
+ compiled = ''
21
+ split('').each do |char|
22
+ compiled += char
23
+ valid_color = compiled if Color.attributes.include?(compiled.to_sym)
24
+ end
25
+
26
+ valid_color
27
+ end
28
+ end
29
+
30
+ attr_reader :original
31
+
32
+ include Color
33
+ def initialize(string, placeholders: {}, force_color: false, wrap_width: 0, color: '', tags_color: '', reset: '')
34
+ Color.coloring = true if force_color
35
+ @colors = nil
36
+ @original = string
37
+ super(Color.reset + string)
38
+
39
+ placeholders.each { |k, v| fill(k, v, wrap_width: wrap_width, color: color, tags_color: tags_color) }
40
+ end
41
+
42
+ ##
43
+ ## Test if string contains any valid %colors
44
+ ##
45
+ ## @return [Boolean] True if colors, False otherwise.
46
+ ##
47
+ def colors?
48
+ scan(/%([a-z]+)/).each do
49
+ return true if Regexp.last_match(1).validate_color
50
+ end
51
+ false
52
+ end
53
+
54
+ def reparse
55
+ @parsed_colors = nil
56
+ end
57
+
58
+ ##
59
+ ## Return string with %colors replaced with escape codes
60
+ ##
61
+ ## @return [String] colorized string
62
+ ##
63
+ def colored
64
+ reparse
65
+ parsed_colors[:string].apply_colors(parsed_colors[:colors])
66
+ end
67
+
68
+ ##
69
+ ## Remove all valid %colors from string
70
+ ##
71
+ ## @return [String] cleaned string
72
+ ##
73
+ def raw
74
+ parsed_colors[:string].uncolor
75
+ end
76
+
77
+ def parsed_colors
78
+ @parsed_colors ||= parse_colors
79
+ end
80
+
81
+ ##
82
+ ## Parse a template string for %colors and return a hash
83
+ ## of colors and string locations
84
+ ##
85
+ ## @return [Hash] Uncolored string and array of colors and locations
86
+ def parse_colors
87
+ working = dup
88
+ color_array = []
89
+
90
+ scan(/(?<!\\)(%([a-z]+))/).each do |color|
91
+ valid_color = color[1].validate_color
92
+ next unless valid_color
93
+
94
+ idx = working.match(/(?<!\\)%#{valid_color}/).begin(0)
95
+ color_array.push({ name: valid_color, color: Color.send(valid_color), index: idx })
96
+ working.sub!(/(?<!\\)%#{valid_color}/, '')
97
+ end
98
+
99
+ { string: working, colors: color_array }
100
+ end
101
+
102
+ ##
103
+ ## Apply a color array to a string
104
+ ##
105
+ ## @param color_array [Array] Array of hashes
106
+ ## containing :name, :color,
107
+ ## :index
108
+ ##
109
+ def apply_colors(color_array)
110
+ str = dup
111
+ color_array.reverse.each do |color|
112
+ c = color[:color].empty? ? Color.send(color[:name]) : color[:color]
113
+ str.insert(color[:index], c)
114
+ end
115
+ str
116
+ end
117
+
118
+ def fill(placeholder, value, wrap_width: 0, color: '', tags_color: '', reset: '')
119
+ reparse
120
+ rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?#{placeholder.sub(/^%/, '')}(?<after>.*?)$/
121
+ ph = raw.match(rx)
122
+
123
+ return unless ph
124
+ placeholder_offset = ph.begin(0)
125
+ last_colors = parsed_colors[:colors].select { |v| v[:index] <= placeholder_offset + 4 }
126
+
127
+ last_color = last_colors.map { |v| v[:color] }.pop(3).join('')
128
+
129
+ sub!(rx) do
130
+ m = Regexp.last_match
131
+
132
+ after = m['after']
133
+
134
+ if value.nil? || value.empty?
135
+ after
136
+ else
137
+ pad = m['width'].to_i
138
+ mark = m['mchar'] || ''
139
+ if placeholder == 'shortdate' && m['width'].nil?
140
+ pad = 13
141
+ end
142
+ indent = nil
143
+ if m['ichar']
144
+ char = m['ichar'] =~ /t/ ? "\t" : ' '
145
+ indent = char * m['icount'].to_i
146
+ end
147
+ indent ||= placeholder =~ /^title/ ? '' : "\t"
148
+ prefix = m['prefix']
149
+ if placeholder =~ /^title/
150
+ color = last_color + color
151
+
152
+ if wrap_width.positive? || pad.positive?
153
+ width = pad.positive? ? pad : wrap_width
154
+
155
+ out = value.gsub(/%/, '\%').strip.wrap(width,
156
+ pad: pad,
157
+ indent: indent,
158
+ offset: placeholder_offset,
159
+ prefix: prefix,
160
+ color: color,
161
+ after: after,
162
+ reset: reset,
163
+ pad_first: false)
164
+ out.highlight_tags!(tags_color, last_color: color) if tags_color && !tags_color.empty?
165
+ out
166
+ else
167
+ out = format("%s%s%#{pad}s%s", prefix, color, value.gsub(/%/, '\%').sub(/\s*$/, ''), after)
168
+ out.highlight_tags!(tags_color, last_color: color) if tags_color && !tags_color.empty?
169
+ out
170
+ end
171
+ elsif placeholder =~ /^note/
172
+ if wrap_width.positive? || pad.positive?
173
+ width = pad.positive? ? pad : wrap_width
174
+ outstring = value.map do |l|
175
+ if l.empty?
176
+ ' '
177
+ else
178
+ line = l.gsub(/%/, '\%').strip.wrap(width, pad: pad, indent: indent, offset: 0, prefix: prefix, color: last_color, after: after, reset: reset, pad_first: true)
179
+ line.highlight_tags!(tags_color, last_color: last_color) unless tags_color.nil? || tags_color.empty?
180
+ "#{line} "
181
+ end
182
+ end.join("\n")
183
+ "\n#{last_color}#{mark}#{outstring} "
184
+ else
185
+ out = format("\n%s%s%s%#{pad}s%s", indent, prefix, last_color, value.join("\n#{indent}#{prefix}").gsub(/%/, '\%').sub(/\s*$/, ''), after)
186
+ out.highlight_tags!(tags_color, last_color: last_color) if tags_color && !tags_color.empty?
187
+ out
188
+ end
189
+ else
190
+ format("%s%#{pad}s%s", prefix, value.gsub(/%/, '\%').sub(/\s*$/, ''), after)
191
+ end
192
+ end
193
+ end
194
+ @parsed_colors = parse_colors
195
+ end
196
+ end
197
+ end
data/lib/doing/time.rb CHANGED
@@ -14,5 +14,37 @@ module Doing
14
14
  strftime('%m/%d/%Y %_I:%M%P')
15
15
  end
16
16
  end
17
+
18
+ def humanize(seconds)
19
+ s = seconds
20
+ m = (s / 60).floor
21
+ s = (s % 60).floor
22
+ h = (m / 60).floor
23
+ m = (m % 60).floor
24
+ d = (h / 24).floor
25
+ h = h % 24
26
+
27
+ output = []
28
+ output.push("#{d} #{'day'.pluralize(d)}") if d.positive?
29
+ output.push("#{h} #{'hour'.pluralize(h)}") if h.positive?
30
+ output.push("#{m} #{'minute'.pluralize(m)}") if m.positive?
31
+ output.push("#{s} #{'second'.pluralize(s)}") if s.positive?
32
+ output.join(', ')
33
+ end
34
+
35
+ def time_ago
36
+ if self > Date.today.to_time
37
+ output = humanize(Time.now - self)
38
+ "#{output} ago"
39
+ elsif self > (Date.today - 1).to_time
40
+ "Yesterday at #{strftime('%_I:%M:%S%P')}"
41
+ elsif self > (Date.today - 6).to_time
42
+ strftime('%a %I:%M:%S%P')
43
+ elsif self.year == Date.today.year
44
+ strftime('%m/%d %I:%M:%S%P')
45
+ else
46
+ strftime('%m/%d/%Y %I:%M:%S%P')
47
+ end
48
+ end
17
49
  end
18
50
  end
data/lib/doing/util.rb CHANGED
@@ -112,20 +112,19 @@ module Doing
112
112
  puts content
113
113
  return
114
114
  end
115
-
115
+ Doing.logger.benchmark(:write_file, :start)
116
116
  file = File.expand_path(file)
117
117
 
118
- if File.exist?(file) && backup
119
- # Create a backup copy for the undo command
120
- FileUtils.cp(file, "#{file}~")
121
- end
118
+ Backup.write_backup(file) if backup
122
119
 
123
120
  File.open(file, 'w+') do |f|
124
121
  f.puts content
125
122
  Doing.logger.debug('Write:', "File written: #{file}")
126
123
  end
127
-
124
+ Doing.logger.benchmark(:_post_write_hook, :start)
128
125
  Hooks.trigger :post_write, file
126
+ Doing.logger.benchmark(:_post_write_hook, :finish)
127
+ Doing.logger.benchmark(:write_file, :finish)
129
128
  end
130
129
 
131
130
  def safe_load_file(filename)
@@ -133,7 +132,7 @@ module Doing
133
132
  end
134
133
 
135
134
  def default_editor
136
- @default_editor = find_default_editor
135
+ @default_editor ||= find_default_editor
137
136
  end
138
137
 
139
138
  def editor_with_args
@@ -0,0 +1,287 @@
1
+ # frozen_string_literal: true
2
+ require 'zlib'
3
+
4
+ module Doing
5
+ module Util
6
+ ## Backup utils
7
+ module Backup
8
+ extend self
9
+ include Util
10
+
11
+ ##
12
+ ## Delete all but most recent 5 backups
13
+ ##
14
+ ## @param limit Maximum number of backups to retain
15
+ ##
16
+ def prune_backups(filename, limit = 10)
17
+ backups = get_backups(filename)
18
+ return unless backups.count > limit
19
+
20
+ backups[limit..-1].each do |file|
21
+ FileUtils.rm(File.join(backup_dir, file))
22
+ end
23
+ end
24
+
25
+ ##
26
+ ## Restore the most recent backup. If a filename is
27
+ ## provided, only backups of that filename will be used.
28
+ ##
29
+ ## @param filename The filename to restore, if
30
+ ## different from default
31
+ ##
32
+ def restore_last_backup(filename = nil, count: 1)
33
+ Doing.logger.benchmark(:restore_backup, :start)
34
+ filename ||= Doing.config.settings['doing_file']
35
+
36
+ result = get_backups(filename).slice(count - 1)
37
+ raise DoingRuntimeError, 'End of undo history' if result.nil?
38
+
39
+ backup_file = File.join(backup_dir, result)
40
+
41
+ save_undone(filename)
42
+ FileUtils.mv(backup_file, filename)
43
+ prune_backups_after(File.basename(backup_file))
44
+ Doing.logger.warn('File update:', "restored from #{result}")
45
+ Doing.logger.benchmark(:restore_backup, :finish)
46
+ end
47
+
48
+ ##
49
+ ## Undo last undo
50
+ ##
51
+ ## @param filename The filename
52
+ ##
53
+ def redo_backup(filename = nil, count: 1)
54
+ filename ||= Doing.config.settings['doing_file']
55
+ # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
56
+ undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
57
+ total = undones.count
58
+ count = total if count > total
59
+
60
+ skipped = undones.slice!(0, count)
61
+ undone = skipped.pop
62
+
63
+ raise DoingRuntimeError, 'End of redo history' if undone.nil?
64
+
65
+ redo_file = File.join(backup_dir, undone)
66
+
67
+ FileUtils.move(redo_file, filename)
68
+
69
+ skipped.each do |f|
70
+ FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
71
+ end
72
+
73
+ Doing.logger.warn('File update:', "restored undo step #{count}/#{total}")
74
+ Doing.logger.debug('Backup:', "#{total - skipped.count - 1} redos remaining")
75
+ end
76
+
77
+ def clear_undone(filename = nil)
78
+ filename ||= Doing.config.settings['doing_file']
79
+ # redo_file = File.join(backup_dir, "undone___#{File.basename(filename)}")
80
+ Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).each do |f|
81
+ FileUtils.rm(File.join(backup_dir, f))
82
+ end
83
+ end
84
+
85
+ ##
86
+ ## Select from recent undos. If a filename is
87
+ ## provided, only backups of that filename will be used.
88
+ ##
89
+ ## @param filename The filename to restore
90
+ ##
91
+ def select_redo(filename = nil)
92
+ filename ||= Doing.config.settings['doing_file']
93
+
94
+ undones = Dir.glob("undone*#{File.basename(filename)}", base: backup_dir).sort
95
+
96
+ raise DoingRuntimeError, 'End of redo history' if undones.empty?
97
+
98
+ total = undones.count
99
+ options = undones.each_with_object([]) do |file, arr|
100
+ d, _base = date_of_backup(file)
101
+ next if d.nil?
102
+
103
+ arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
104
+ end
105
+
106
+ backup_file = show_menu(options, filename)
107
+ idx = undones.index(File.basename(backup_file))
108
+ skipped = undones.slice!(idx, undones.count - idx)
109
+ undone = skipped.shift
110
+
111
+ redo_file = File.join(backup_dir, undone)
112
+
113
+ FileUtils.move(redo_file, filename)
114
+
115
+ skipped.each do |f|
116
+ FileUtils.mv(File.join(backup_dir, f), File.join(backup_dir, f.sub(/^undone/, '')))
117
+ end
118
+
119
+ Doing.logger.warn('File update:', "restored undo step #{idx}/#{total}")
120
+ Doing.logger.debug('Backup:', "#{total - skipped.count - 1} redos remaining")
121
+ end
122
+
123
+ ##
124
+ ## Select from recent backups. If a filename is
125
+ ## provided, only backups of that filename will be used.
126
+ ##
127
+ ## @param filename The filename to restore
128
+ ##
129
+ def select_backup(filename = nil)
130
+ filename ||= Doing.config.settings['doing_file']
131
+
132
+ options = get_backups(filename).each_with_object([]) do |file, arr|
133
+ d, _base = date_of_backup(file)
134
+ next if d.nil?
135
+ arr.push("#{d.time_ago}\t#{File.join(backup_dir, file)}")
136
+ end
137
+
138
+ backup_file = show_menu(options, filename)
139
+ write_to_file(File.join(backup_dir, "undone___#{File.basename(filename)}"), IO.read(filename), backup: false)
140
+ FileUtils.mv(backup_file, filename)
141
+ prune_backups_after(File.basename(backup_file))
142
+ Doing.logger.warn('File update:', "restored from #{backup_file}")
143
+ end
144
+
145
+ def show_menu(options, filename)
146
+ if TTY::Which.which('colordiff')
147
+ preview = 'colordiff -U 1'
148
+ pipe = '| awk "(NR>2)"'
149
+ elsif TTY::Which.which('git')
150
+ preview = 'git --no-pager diff -U1 --color=always --minimal --word-diff'
151
+ pipe = ' | awk "(NR>4)"'
152
+ else
153
+ preview = 'diff -U 1'
154
+ pipe = if TTY::Which.which('delta')
155
+ ' | delta --no-gitconfig --syntax-theme=1337'
156
+ elsif TTY::Which.which('diff-so-fancy')
157
+ ' | diff-so-fancy'
158
+ elsif TTY::Which.which('ydiff')
159
+ ' | ydiff -c always --wrap < /dev/tty'
160
+ else
161
+ cmd = 'sed -e "s/^-/`echo -e "\033[31m"`-/;s/^+/`echo -e "\033[32m"`+/;s/^@/`echo -e "\033[34m"`@/;s/\$/`echo -e "\033[0m"`/"'
162
+ "| bash -c #{Shellwords.escape(cmd)}"
163
+ end
164
+ pipe += ' | awk "(NR>2)"'
165
+ end
166
+
167
+ result = Doing::Prompt.choose_from(options,
168
+ prompt: 'Select a backup to restore',
169
+ sorted: false,
170
+ fzf_args: [
171
+ '--delimiter="\t"',
172
+ '--with-nth=1',
173
+ %(--preview='#{preview} "#{filename}" {2} #{pipe}'),
174
+ '--disabled',
175
+ '--height=10',
176
+ '--preview-window="right,70%,nowrap,follow"',
177
+ '--header="Select a revision to restore"'
178
+ ])
179
+ raise UserCancelled unless result
180
+
181
+ result.strip.split(/\t/).last
182
+ end
183
+
184
+ ##
185
+ ## Writes a copy of the content to a dated backup file
186
+ ## in a hidden directory
187
+ ##
188
+ ## @param content The data to back up
189
+ ##
190
+ def write_backup(filename = nil)
191
+ Doing.logger.benchmark(:_write_backup, :start)
192
+ filename ||= Doing.config.settings['doing_file']
193
+
194
+ unless File.exist?(filename)
195
+ Doing.logger.debug('Backup:', "original file doesn't exist (#{filename})")
196
+ return
197
+ end
198
+
199
+ backup_file = File.join(backup_dir, "#{timestamp_filename}___#{File.basename(filename)}")
200
+ # compressed = Zlib::Deflate.deflate(content)
201
+ # Zlib::GzipWriter.open(backup_file + '.gz') do |gz|
202
+ # gz.write(IO.read(filename))
203
+ # end
204
+
205
+ FileUtils.cp(filename, backup_file)
206
+
207
+ prune_backups(filename, 15)
208
+ clear_undone(filename)
209
+ Doing.logger.benchmark(:_write_backup, :finish)
210
+ end
211
+
212
+ private
213
+
214
+ def timestamp_filename
215
+ Time.now.strftime('%Y-%m-%d_%H.%M.%S')
216
+ end
217
+
218
+ def get_backups(filename = nil)
219
+ filename ||= Doing.config.settings['doing_file']
220
+ backups = Dir.glob("*___#{File.basename(filename)}", base: backup_dir).sort.reverse
221
+ backups.delete_if { |f| f =~ /^undone/ }
222
+ end
223
+
224
+ def save_undone(filename = nil)
225
+ filename ||= Doing.config.settings['doing_file']
226
+ undone_file = File.join(backup_dir, "undone#{timestamp_filename}___#{File.basename(filename)}")
227
+ FileUtils.cp(filename, undone_file)
228
+ end
229
+
230
+ ##
231
+ ## Retrieve date from backup filename
232
+ ##
233
+ ## @param filename The filename
234
+ ##
235
+ def date_of_backup(filename)
236
+ m = filename.match(/^(?:undone)?(?<date>\d{4}-\d{2}-\d{2})_(?<time>\d{2}\.\d{2}\.\d{2})___(?<file>.*?)$/)
237
+ return nil if m.nil?
238
+
239
+ [Time.parse("#{m['date']} #{m['time'].gsub(/\./, ':')}"), m['file']]
240
+ end
241
+
242
+ ##
243
+ ## Return a location for storing backups, creating if needed
244
+ ##
245
+ ## @return Path to backup directory
246
+ ##
247
+ def backup_dir
248
+ @backup_dir ||= create_backup_dir
249
+ end
250
+
251
+ def create_backup_dir
252
+ dir = File.expand_path(Doing.config.settings['backup_dir']) || File.join(user_home, '.doing_backup')
253
+ if File.exist?(dir) && !File.directory?(dir)
254
+ raise DoingRuntimeError, "Backup error: #{dir} is not a directory"
255
+
256
+ end
257
+
258
+ unless File.exist?(dir)
259
+ FileUtils.mkdir_p(dir)
260
+ Doing.logger.warn('Backup:', "backup directory created at #{dir}")
261
+ end
262
+
263
+ dir
264
+ end
265
+
266
+ ##
267
+ ## Delete backups newer than selected filename
268
+ ##
269
+ ## @param filename The filename
270
+ ##
271
+ def prune_backups_after(filename)
272
+ target_date, base = date_of_backup(filename)
273
+ return if target_date.nil?
274
+
275
+ counter = 0
276
+ get_backups(base).each do |file|
277
+ date, _base = date_of_backup(file)
278
+ if date && target_date < date
279
+ FileUtils.mv(File.join(backup_dir, file), File.join(backup_dir, "undone#{file}"))
280
+ counter += 1
281
+ end
282
+ end
283
+ Doing.logger.debug('Backup:', "deleted #{counter} files newer than restored backup")
284
+ end
285
+ end
286
+ end
287
+ end
data/lib/doing/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Doing
2
- VERSION = '2.1.1pre'
2
+ VERSION = '2.1.5pre'
3
3
  end