postrunner 0.1.0 → 0.2.0

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
  SHA1:
3
- metadata.gz: 99ae2bd8b3b82d191caf48131b2859c4f4e29720
4
- data.tar.gz: ca9b0777ec1fe947bd2aeefa969b7c6e45d52d19
3
+ metadata.gz: bd33c5715662822de4739db4f57eace5c10e08fa
4
+ data.tar.gz: 6d64e7cd63503bf1a480c20ff2f3c1b2763e643b
5
5
  SHA512:
6
- metadata.gz: 89b8afc287b91f5f787e00da5b13642f2e2b86fd73296bd7d70580069698ce36c794773d16377d029eedb92f16293722c70605e79ab100c67442b51e4b7efd70
7
- data.tar.gz: 2089af8448f6151f7d3dfd1a1e9ccfeaa4849fa59e1f9b3b30ea8df798ac8b673e639beb494d07aef40c234abe3999a39764523defacf3524e61ee0263cfc2d8
6
+ metadata.gz: 1ce585498cb68bc2f2d623fd22058eb12187f247d17c39ed8e23f8f995ad44e0178e75a319a46d0de9f899de6f56b93d849ea669350b00e97faa638da19f7491
7
+ data.tar.gz: 3104d9e1f00f97629d85841a37838320037d95db794d77656cfc4d503c01ccec989e6f3ec13bd842fafb41587371fa8d23a333b1d7ee3832d242c896f96286d5
data/README.md CHANGED
@@ -32,12 +32,13 @@ full path to the directory that contains your FIT files. You can then
32
32
  import all files on the device.
33
33
 
34
34
  ```
35
- $ postrunner import /var/run/media/user/GARMIN/GARMIN/ACTIVITY/
35
+ $ postrunner import /run/media/$USER/GARMIN/GARMIN/ACTIVITY/
36
36
  ```
37
37
 
38
38
  The above command assumes that your device is mounted as
39
- /var/run/media/user. Please replace this with the path to your device.
40
- Files that have been imported previously will not be imported again.
39
+ /run/media/$USER. Please replace $USER with your login name and the
40
+ path with the path to your device. Files that have been imported
41
+ previously will not be imported again.
41
42
 
42
43
  ### Viewing FIT file data on the console
43
44
 
@@ -280,14 +280,16 @@ module PostRunner
280
280
  last_distance = nil
281
281
 
282
282
  @fit_activity.records.each do |record|
283
+ if record.timestamp.nil?
284
+ # All records must have a valid timestamp
285
+ Log.warn "Found a record without a valid timestamp"
286
+ return
287
+ end
283
288
  if record.distance.nil?
284
289
  # All records must have a valid distance mark or the activity does
285
290
  # not qualify for a personal record.
286
- Log.warn "Found a record without a valid distance"
287
- return
288
- end
289
- if record.timestamp.nil?
290
- Log.warn "Found a record without a valid timestamp"
291
+ Log.warn "Found a record at #{record.timestamp} without a " +
292
+ "valid distance"
291
293
  return
292
294
  end
293
295
 
@@ -67,7 +67,8 @@ module PostRunner
67
67
  def body(doc)
68
68
  @view.body {
69
69
  doc.div({ :class => 'main' }) {
70
- ViewFrame.new('Activities', 900, generate_table).to_html(doc)
70
+ ViewFrame.new('activities', 'Activities', 900,
71
+ generate_table).to_html(doc)
71
72
  }
72
73
  }
73
74
  end
@@ -33,17 +33,30 @@ module PostRunner
33
33
  end
34
34
 
35
35
  def to_s
36
- summary.to_s + "\n" + laps.to_s
36
+ summary.to_s + "\n" +
37
+ (@activity.note ? note.to_s + "\n" : '') +
38
+ laps.to_s
37
39
  end
38
40
 
39
41
  def to_html(doc)
40
42
  width = 600
41
- ViewFrame.new("Activity: #{@name}", width, summary).to_html(doc)
42
- ViewFrame.new('Laps', width, laps).to_html(doc)
43
+ ViewFrame.new('activity', "Activity: #{@name}",
44
+ width, summary).to_html(doc)
45
+ ViewFrame.new('note', 'Note', width, note,
46
+ true).to_html(doc) if @activity.note
47
+ ViewFrame.new('laps', 'Laps', width, laps, true).to_html(doc)
43
48
  end
44
49
 
45
50
  private
46
51
 
52
+ def note
53
+ t = FlexiTable.new
54
+ t.enable_frame(false)
55
+ t.body
56
+ t.row([ @activity.note ])
57
+ t
58
+ end
59
+
47
60
  def summary
48
61
  session = @fit_activity.sessions[0]
49
62
 
@@ -51,14 +51,23 @@ module PostRunner
51
51
  private
52
52
 
53
53
  def generate_html(doc)
54
- doc.head { doc.style(style) }
54
+ doc.unique(:activityview_style) {
55
+ doc.head {
56
+ [ 'jquery/jquery-2.1.1.min.js', 'flot/jquery.flot.js',
57
+ 'flot/jquery.flot.time.js' ].each do |js|
58
+ doc.script({ 'language' => 'javascript',
59
+ 'type' => 'text/javascript', 'src' => js })
60
+ end
61
+ doc.style(style)
62
+ }
63
+ }
55
64
  #doc.meta({ 'name' => 'viewport',
56
65
  # 'content' => 'width=device-width, ' +
57
66
  # 'initial-scale=1.0, maximum-scale=1.0, ' +
58
67
  # 'user-scalable=0' })
59
68
 
60
69
  body {
61
- doc.body({ :onload => 'init()' }) {
70
+ doc.body {
62
71
  # The main area with the 2 column layout.
63
72
  doc.div({ :class => 'main' }) {
64
73
  doc.div({ :class => 'left_col' }) {
@@ -157,15 +157,9 @@ module PostRunner
157
157
  def to_html(doc)
158
158
  doc.unique(:chartview_style) {
159
159
  doc.head {
160
- [ 'jquery/jquery-2.1.1.min.js', 'flot/jquery.flot.js',
161
- 'flot/jquery.flot.time.js' ].each do |js|
162
- doc.script({ 'language' => 'javascript',
163
- 'type' => 'text/javascript', 'src' => js })
164
- end
165
160
  doc.style(style)
166
161
  }
167
162
  }
168
-
169
163
  doc.script(java_script)
170
164
  @charts.each do |chart|
171
165
  label = chart[:label] + (chart[:unit] ? " (#{chart[:unit]})" : '')
@@ -333,7 +327,7 @@ EOT
333
327
  " inverseTransform: function (v) { return -v; } }"
334
328
  else
335
329
  # Set the minimum slightly below the lowest found value.
336
- s << ", yaxis: { min: #{0.9 * min_value} }"
330
+ s << ", yaxis: { min: #{0.9 * min_value} }" if min_value > 0.0
337
331
  end
338
332
  s << "});\n"
339
333
  s << lap_mark_labels(chart_id, start_time)
@@ -411,7 +405,7 @@ EOT
411
405
  # Don't plot frame for graph without data.
412
406
  return if @empty_charts[field]
413
407
 
414
- ViewFrame.new(title) {
408
+ ViewFrame.new("#{field}_chart", title, 600, nil, true) {
415
409
  doc.div({ 'id' => "#{field}_chart", 'class' => 'chart-placeholder'})
416
410
  }.to_html(doc)
417
411
  end
@@ -39,7 +39,8 @@ module PostRunner
39
39
  end
40
40
 
41
41
  def to_html(doc)
42
- ViewFrame.new("Data Sources", 1210, data_sources).to_html(doc)
42
+ ViewFrame.new('data_sources', "Data Sources", 1210, data_sources,
43
+ true).to_html(doc)
43
44
  end
44
45
 
45
46
  private
@@ -44,7 +44,7 @@ module PostRunner
44
44
  end
45
45
 
46
46
  def to_html(doc)
47
- ViewFrame.new('Devices', 600, devices).to_html(doc)
47
+ ViewFrame.new('devices', 'Devices', 600, devices, true).to_html(doc)
48
48
  end
49
49
 
50
50
  def to_s
@@ -42,7 +42,7 @@ module PostRunner
42
42
  # Add the list as HTML table to the specified doc.
43
43
  # @param doc [HTMLBuilder] HTML document
44
44
  def to_html(doc)
45
- ViewFrame.new("Events", 600, list).to_html(doc)
45
+ ViewFrame.new('events', 'Events', 600, list, true).to_html(doc)
46
46
  end
47
47
 
48
48
  private
@@ -96,8 +96,9 @@ module PostRunner
96
96
  'all' => 'All'
97
97
  }
98
98
 
99
- po_attr :device, :fit_file_name, :norecord, :name, :sport, :sub_sport,
100
- :timestamp, :total_distance, :total_timer_time, :avg_speed
99
+ po_attr :device, :fit_file_name, :norecord, :name, :note, :sport,
100
+ :sub_sport, :timestamp, :total_distance, :total_timer_time,
101
+ :avg_speed
101
102
  attr_reader :fit_activity
102
103
 
103
104
  # Create a new FFS_Activity object.
@@ -209,6 +210,8 @@ module PostRunner
209
210
  case attribute
210
211
  when 'name'
211
212
  self.name = value
213
+ when 'note'
214
+ self.note = value
212
215
  when 'type'
213
216
  load_fit_file
214
217
  unless ActivityTypes.values.include?(value)
@@ -150,6 +150,7 @@ module PostRunner
150
150
  def rename_activity(activity, name)
151
151
  activity.set('name', name)
152
152
  generate_html_index_pages
153
+ @store['records'].generate_html_reports if activity.has_records?
153
154
  end
154
155
 
155
156
  # Set the specified attribute of the given activity to a new value.
@@ -32,8 +32,14 @@ module PostRunner
32
32
  create_node('meta', { 'http-equiv' => 'Content-Type',
33
33
  'content' => 'text/html; charset=utf-8' })
34
34
  create_node('title', title)
35
+ @init_script = create_node('script', <<EOT
36
+ function postrunner_init() {
37
+ };
38
+ EOT
39
+ )
35
40
  }
36
41
  @body = create_node('body')
42
+ @body['onload'] = 'postrunner_init()'
37
43
  }
38
44
  @node_stack << @html
39
45
  @node_stack << @body
@@ -62,6 +68,12 @@ module PostRunner
62
68
  end
63
69
  end
64
70
 
71
+ def body_init_script(script)
72
+ # We have to insert the new script snippet after the last snippet and
73
+ # before the tailing "};\n".
74
+ @init_script.content = @init_script.text[0..-4] + script + "\n};\n"
75
+ end
76
+
65
77
  # Only execute the passed block if the provided tag has not been added
66
78
  # yet.
67
79
  def unique(tag)
@@ -40,28 +40,27 @@ module PostRunner
40
40
 
41
41
  return if (args = parse_options(args)).nil?
42
42
 
43
- create_directory(@db_dir, 'PostRunner data')
44
- @db = PEROBS::Store.new(File.join(@db_dir, 'database'))
45
- # Create a hash to store configuration data in the store unless it
46
- # exists already.
47
- cfg = (@db['config'] ||= @db.new(PEROBS::Hash))
48
- cfg['unit_system'] ||= :metric
49
- cfg['version'] ||= VERSION
50
- # We always override the data_dir as the user might have moved the data
51
- # directory. The only reason we store it in the DB is to have it
52
- # available throught the application.
53
- cfg['data_dir'] = @db_dir
54
- # Always update html_dir setting so that the DB directory can be moved
55
- # around by the user.
56
- cfg['html_dir'] = File.join(@db_dir, 'html')
57
-
58
- setup_directories
59
- if (errors = @db.check) != 0
60
- Log.fatal "Postrunner database is corrupted: #{errors} errors found"
43
+ unless $DEBUG
44
+ Kernel.trap('INT') do
45
+ begin
46
+ Log.fatal('Aborting on user request!')
47
+ rescue RuntimeError
48
+ exit 1
49
+ end
50
+ end
61
51
  end
62
- execute_command(args)
63
52
 
64
- @db.sync
53
+ begin
54
+ main(args)
55
+ rescue Exception => e
56
+ if e.is_a?(SystemExit) || e.is_a?(Interrupt)
57
+ $stderr.puts e.backtrace.join("\n") if $DEBUG
58
+ else
59
+ Log.fatal("#{e}\n#{e.backtrace.join("\n")}\n\n" +
60
+ "#{'*' * 79}\nYou have triggered a bug in PostRunner " +
61
+ "#{VERSION}!")
62
+ end
63
+ end
65
64
  end
66
65
 
67
66
  private
@@ -123,6 +122,9 @@ EOT
123
122
  'Directory for the activity database and related files') do |d|
124
123
  @db_dir = d
125
124
  end
125
+ opts.on('--debug', 'Enable debug mode') do
126
+ $DEBUG = true
127
+ end
126
128
  opts.on('-v', '--verbose',
127
129
  'Show internal messages helpful for debugging problems') do
128
130
  Log.level = Logger::DEBUG
@@ -176,6 +178,7 @@ set <attribute> <value> <ref>
176
178
  name: The activity name (defaults to FIT file name)
177
179
  norecord: Ignore all records from this activity (value must true
178
180
  or false)
181
+ note: A comment or summary of the activity
179
182
  type: The type of the activity
180
183
  subtype: The subtype of the activity
181
184
 
@@ -218,6 +221,31 @@ EOT
218
221
  end
219
222
  end
220
223
 
224
+ def main(args)
225
+ create_directory(@db_dir, 'PostRunner data')
226
+ @db = PEROBS::Store.new(File.join(@db_dir, 'database'))
227
+ # Create a hash to store configuration data in the store unless it
228
+ # exists already.
229
+ cfg = (@db['config'] ||= @db.new(PEROBS::Hash))
230
+ cfg['unit_system'] ||= :metric
231
+ cfg['version'] ||= VERSION
232
+ # We always override the data_dir as the user might have moved the data
233
+ # directory. The only reason we store it in the DB is to have it
234
+ # available throught the application.
235
+ cfg['data_dir'] = @db_dir
236
+ # Always update html_dir setting so that the DB directory can be moved
237
+ # around by the user.
238
+ cfg['html_dir'] = File.join(@db_dir, 'html')
239
+
240
+ setup_directories
241
+ if $DEBUG && (errors = @db.check) != 0
242
+ Log.fatal "Postrunner database is corrupted: #{errors} errors found"
243
+ end
244
+ execute_command(args)
245
+
246
+ @db.sync
247
+ end
248
+
221
249
  def setup_directories
222
250
  create_directory(@db['config']['html_dir'], 'HTML output')
223
251
 
@@ -425,7 +453,13 @@ EOT
425
453
  when :rename
426
454
  @ffs.rename_activity(activity, @name)
427
455
  when :set
428
- @ffs.set_activity_attribute(activity, @attribute, @value)
456
+ if @attribute == 'name'
457
+ # We have to handle the 'name' attribute as special case as we have
458
+ # to update some HTML reports as well.
459
+ @ffs.rename_activity(activity, @value)
460
+ else
461
+ @ffs.set_activity_attribute(activity, @attribute, @value)
462
+ end
429
463
  when :show
430
464
  activity.show
431
465
  when :sources
@@ -52,13 +52,14 @@ module PostRunner
52
52
  frame_width = 800
53
53
 
54
54
  @doc.div({ :class => 'main' }) {
55
- ViewFrame.new("All-time #{@sport_name} Records",
55
+ ViewFrame.new('all_time_records', "All-time #{@sport_name} Records",
56
56
  frame_width, @records.all_time).to_html(@doc)
57
57
 
58
58
  @records.yearly.sort{ |y1, y2| y2[0].to_i <=> y1[0].to_i }.
59
59
  each do |year, record|
60
60
  next if record.empty?
61
- ViewFrame.new("#{year} #{@sport_name} Records",
61
+ ViewFrame.new("year_#{year}_records",
62
+ "#{year} #{@sport_name} Records",
62
63
  frame_width, record).to_html(@doc)
63
64
  end
64
65
  }
@@ -43,9 +43,10 @@ module PostRunner
43
43
  doc.script({ 'src' => 'postrunner/trackview.js' })
44
44
  }
45
45
  doc.script(java_script)
46
+ doc.body_init_script('pr_trackview_init_xy()')
46
47
  }
47
48
 
48
- ViewFrame.new('Map', 600) {
49
+ ViewFrame.new('map', 'Map', 600, nil, true) {
49
50
  doc.div({ 'id' => 'map', 'class' => 'trackmap' })
50
51
  }.to_html(doc)
51
52
  end
@@ -75,9 +76,9 @@ EOT
75
76
  <<"EOT"
76
77
  #{track_points}
77
78
  #{lap_markers}
78
- function init() {
79
+ function pr_trackview_init_xy() {
79
80
  pr_trackview_init(#{center_long}, #{center_lat});
80
- }
81
+ };
81
82
  EOT
82
83
  end
83
84
 
@@ -26,7 +26,8 @@ module PostRunner
26
26
  def to_html(doc)
27
27
  return nil if @fit_activity.user_profiles.empty?
28
28
 
29
- ViewFrame.new('User Profile', 600, profile).to_html(doc)
29
+ ViewFrame.new('user_profile', 'User Profile', 600, profile,
30
+ true).to_html(doc)
30
31
  end
31
32
 
32
33
  def to_s
@@ -16,16 +16,21 @@ module PostRunner
16
16
  class ViewFrame
17
17
 
18
18
  # Create a ViewFrame object.
19
+ # @param id [String] ID that is unique with regards to the rendered HTML
20
+ # page.
19
21
  # @param title [String] Title/heading of the framed box
20
22
  # @param width [Fixnum or nil] Width of the frame. Use nil to set no
21
23
  # width.
22
24
  # @param content [Any object that respons to to_html] Object to frame
23
25
  # @param &block [HTMLBuilder actions]
24
- def initialize(title, width = 600, content = nil, &block)
26
+ def initialize(id, title, width = 600, content = nil, hide_button = false,
27
+ &block)
28
+ @id = id
25
29
  @title = title
26
30
  @content = content
27
31
  @block = block
28
32
  @width = width
33
+ @hide_button = hide_button
29
34
  end
30
35
 
31
36
  # Generate the HTML code for the frame and the enclosing content.
@@ -33,16 +38,41 @@ module PostRunner
33
38
  def to_html(doc)
34
39
  doc.unique(:viewframe_style) {
35
40
  # Add the necessary style sheet snippets to the document head.
36
- doc.head { doc.style(style) }
41
+ doc.head {
42
+ doc.style(style)
43
+ doc.script({ 'src' => 'postrunner/postrunner.js' })
44
+ }
45
+ }
46
+ doc.head {
47
+ doc.script(<<"EOT"
48
+ function init_viewframe_#{@id}() {
49
+ if (readCookie('postrunner_checkbox_#view_#{@id}') == 'false') {
50
+ $('#checkbox-#{@id}').attr('checked', false);
51
+ $('#view_#{@id}').hide();
52
+ } else {
53
+ $('#checkbox-#{@id}').attr('checked', true);
54
+ $('#view_#{@id}').show();
55
+ };
56
+ };
57
+ EOT
58
+ )
59
+ doc.body_init_script("init_viewframe_#{@id}();")
37
60
  }
38
61
 
39
62
  attr = { 'class' => 'widget_frame' }
40
63
  attr['style'] = "width: #{@width}px" if @width
41
64
  doc.div(attr) {
42
65
  doc.div({ 'class' => 'widget_frame_title' }) {
43
- doc.b(@title)
66
+ doc.div('style' => 'float:left') { doc.b(@title) }
67
+ if @hide_button
68
+ doc.div('style' => 'float:right') {
69
+ doc.input('id' => "checkbox-#{@id}", 'type' => "checkbox",
70
+ 'onclick' =>
71
+ "pr_view_frame_toggle(this, \"#view_#{@id}\");")
72
+ }
73
+ end
44
74
  }
45
- doc.div {
75
+ doc.div('class' => 'view_frame', 'id' => "view_#{@id}") {
46
76
  # The @content holds an object that must respond to to_html to
47
77
  # generate the HTML code.
48
78
  if @content
@@ -81,8 +111,10 @@ module PostRunner
81
111
  }
82
112
  .widget_frame_title {
83
113
  font-size:13pt;
84
- padding-bottom: 5px;
85
- text-align: left;
114
+ height: 23px;
115
+ }
116
+ .view_frame {
117
+ padding-top: 5px;
86
118
  }
87
119
  EOT
88
120
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # = version.rb -- PostRunner - Manage the data from your Garmin sport devices.
5
5
  #
6
- # Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
6
+ # Copyright (c) 2014, 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
7
7
  #
8
8
  # This program is free software; you can redistribute it and/or modify
9
9
  # it under the terms of version 2 of the GNU General Public License as
@@ -11,5 +11,5 @@
11
11
  #
12
12
 
13
13
  module PostRunner
14
- VERSION = "0.1.0"
14
+ VERSION = "0.2.0"
15
15
  end
@@ -1,7 +1,7 @@
1
1
  /*
2
- * TrackView.rb -- PostRunner - Manage the data from your Garmin sport devices.
2
+ * TrackView.js -- PostRunner - Manage the data from your Garmin sport devices.
3
3
  *
4
- * Copyright (c) 2014 by Chris Schlaeger <cs@taskjuggler.org>
4
+ * Copyright (c) 2014, 2015, 2016 by Chris Schlaeger <cs@taskjuggler.org>
5
5
  *
6
6
  * This program is free software; you can redistribute it and/or modify
7
7
  * it under the terms of version 2 of the GNU General Public License as
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ["lib"]
20
20
  spec.required_ruby_version = '>=2.0'
21
21
 
22
- spec.add_dependency 'fit4ruby', '~> 0.0.9'
22
+ spec.add_dependency 'fit4ruby', '~> 0.0.11'
23
23
  spec.add_dependency 'perobs', '~> 2.2'
24
24
  spec.add_dependency 'nokogiri', '~> 1.6'
25
25
 
@@ -0,0 +1,171 @@
1
+ require 'time'
2
+
3
+ desc 'Generate the CHANGELOG file'
4
+ task :changelog do
5
+
6
+ class Entry
7
+
8
+ attr_reader :type
9
+
10
+ def initialize(ref, author, time, message)
11
+ @ref = ref
12
+ @author = author
13
+ @time = time
14
+ @message = message
15
+ if (m = /New: (.*)/i.match(@message))
16
+ @type = :feature
17
+ @message = m[1]
18
+ elsif (m = /Fix: (.*)/i.match(@message))
19
+ @type = :bugfix
20
+ @message = m[1]
21
+ else
22
+ @type = :other
23
+ end
24
+ end
25
+
26
+ def to_s
27
+ " * #{@message}\n"
28
+ end
29
+
30
+ end
31
+
32
+ class Release
33
+
34
+ attr_reader :date, :version, :tag
35
+
36
+ def initialize(tag, predecessor)
37
+ @tag = tag
38
+ # We only support release tags in the form X.X.X
39
+ @version = /\d+\.\d+\.\d+/.match(tag)
40
+
41
+ # Construct a Git range.
42
+ interval = predecessor ? "#{predecessor.tag}..#{@tag}" : @tag
43
+
44
+ # Get the date of the release
45
+ git_show_tag = `git show #{tag}`.encode('UTF-8', invalid: :replace,
46
+ undef: :replace)
47
+ date = Time.parse(/Date: (.*)/.match(git_show_tag)[1]).utc
48
+ @date = date.strftime("%Y-%m-%d")
49
+
50
+ @entries = []
51
+ # Use -z option for git-log to get 0 bytes as separators.
52
+ `git log -z #{interval}`.split("\0").each do |commit|
53
+ # We ignore merges.
54
+ next if commit =~ /^Merge: \d*/
55
+
56
+ ref, author, time, _, message = commit.split("\n", 5)
57
+ ref = ref[/commit ([0-9a-f]+)/, 1]
58
+ author = author[/Author: (.*)/, 1].strip
59
+ time = Time.parse(time[/Date: (.*)/, 1]).utc
60
+ # Eleminate git-svn-id: lines
61
+ message.gsub!(/git-svn-id: .*\n/, '')
62
+ # Eliminate Signed-off-by: lines
63
+ message.gsub!(/Signed-off-by: .*\n/, '')
64
+ message.strip!
65
+ @entries << Entry.new(ref, author, time, message)
66
+ end
67
+ end
68
+
69
+ def empty?
70
+ @entries.empty?
71
+ end
72
+
73
+ def to_s
74
+ s = ''
75
+ if hasFeatures? || hasFixes?
76
+ if hasFeatures?
77
+ s << "== New Features\n\n"
78
+ @entries.each do |entry|
79
+ s << entry.to_s if entry.type == :feature
80
+ end
81
+ s << "\n"
82
+ end
83
+ if hasFixes?
84
+ s << "== Bug Fixes\n\n"
85
+ @entries.each do |entry|
86
+ s << entry.to_s if entry.type == :bugfix
87
+ end
88
+ s << "\n"
89
+ end
90
+ else
91
+ @entries.each do |entry|
92
+ s << entry.to_s
93
+ end
94
+ end
95
+ s
96
+ end
97
+
98
+ private
99
+
100
+ def hasFeatures?
101
+ @entries.each do |entry|
102
+ return true if entry.type == :feature
103
+ end
104
+ false
105
+ end
106
+
107
+ def hasFixes?
108
+ @entries.each do |entry|
109
+ return true if entry.type == :bugfix
110
+ end
111
+ false
112
+ end
113
+
114
+ end
115
+
116
+ class ChangeLog
117
+
118
+ def initialize
119
+ @releases = []
120
+ predecessor = nil
121
+ getReleaseVersions.each do |version|
122
+ @releases << (predecessor = Release.new(version, predecessor))
123
+ end
124
+ end
125
+
126
+ def to_s
127
+ s = ''
128
+ @releases.reverse.each do |release|
129
+ next if release.empty?
130
+
131
+ # We use RDOC markup syntax to generate a title
132
+ if release.version
133
+ s << "= Release #{release.version} (#{release.date})\n\n"
134
+ else
135
+ s << "= Next Release (Some Day)\n\n"
136
+ end
137
+ s << release.to_s + "\n"
138
+ end
139
+ s
140
+ end
141
+
142
+ private
143
+
144
+ # 'git tag' is not sorted numerically. This function implements a
145
+ # numerical comparison for tag versions of the format 'release-X.X.X'. X
146
+ # can be a multi-digit number.
147
+ def compareTags(a, b)
148
+
149
+ def versionToComparable(v)
150
+ /\d+\.\d+\.\d+/.match(v)[0].split('.').map{ |l| sprintf("%03d", l.to_i)}.
151
+ join('.')
152
+ end
153
+
154
+ versionToComparable(a) <=> versionToComparable(b)
155
+ end
156
+
157
+ def getReleaseVersions
158
+ # Get list of release tags from Git repository
159
+ releaseVersions = `git tag`.split("\n").map { |r| r.chomp }.
160
+ delete_if { |r| ! (/v\d+\.\d+\.\d+/ =~ r) }.
161
+ sort{ |a, b| compareTags(a, b) }
162
+ releaseVersions << 'HEAD'
163
+ end
164
+
165
+ end
166
+
167
+ File.open('CHANGELOG', 'w+') do |changelog|
168
+ changelog.puts ChangeLog.new.to_s
169
+ end
170
+
171
+ end
@@ -0,0 +1,7 @@
1
+ require 'rake'
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc 'Run all RSpec tests in the spec directory'
5
+ RSpec::Core::RakeTask.new(:test) do |t|
6
+ t.pattern = 'spec/*_spec.rb'
7
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postrunner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Schlaeger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-29 00:00:00.000000000 Z
11
+ date: 2016-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fit4ruby
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.0.9
19
+ version: 0.0.11
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.0.9
26
+ version: 0.0.11
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: perobs
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -304,6 +304,8 @@ files:
304
304
  - spec/PostRunner_spec.rb
305
305
  - spec/View_spec.rb
306
306
  - spec/spec_helper.rb
307
+ - tasks/changelog.rake
308
+ - tasks/test.rake
307
309
  homepage: https://github.com/scrapper/postrunner
308
310
  licenses:
309
311
  - GNU GPL version 2