vclog 1.0.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.
data/HISTORY ADDED
@@ -0,0 +1,20 @@
1
+ = CHANGE HISTORY
2
+
3
+ == 1.0.0 / 2009-08-03
4
+
5
+ This is the first "production" release of VCLog.
6
+
7
+ * 2 Major Enhancements
8
+
9
+ * Improved command line interface.
10
+ * Added output option to save changelog.
11
+
12
+
13
+ == 0.1.0 / 2006-06-05
14
+
15
+ This is the initial version of vclog.
16
+
17
+ * 1 Major Enhancement
18
+
19
+ * Happy Birthday
20
+
data/MANIFEST ADDED
@@ -0,0 +1,32 @@
1
+ test
2
+ RELEASE
3
+ README
4
+ HISTORY
5
+ meta
6
+ meta/created
7
+ meta/repository
8
+ meta/homepage
9
+ meta/summary
10
+ meta/package
11
+ meta/title
12
+ meta/version
13
+ meta/license
14
+ meta/sitemap
15
+ meta/mailinglist
16
+ meta/authors
17
+ meta/requires
18
+ meta/project
19
+ meta/description
20
+ lib
21
+ lib/vclog
22
+ lib/vclog/changelog.rb
23
+ lib/vclog/core_ext.rb
24
+ lib/vclog/vcs
25
+ lib/vclog/vcs/darcs.rb
26
+ lib/vclog/vcs/git.rb
27
+ lib/vclog/vcs/svn.rb
28
+ lib/vclog/vcs/hg.rb
29
+ lib/vclog/vcs.rb
30
+ bin
31
+ bin/vclog
32
+ COPYING
data/README ADDED
@@ -0,0 +1,38 @@
1
+ = VCLog
2
+
3
+ * http://proutils.rubyforge.org
4
+ * http://proutils.rubyforge.org/vclog
5
+
6
+
7
+ == DESCRIPTION
8
+
9
+ VClog is an cross-VCS/SCM changelog generator.
10
+
11
+
12
+ == SYNOPSIS
13
+
14
+ VCLog generates changelogs. It supports a few different formats. The default
15
+ format is standard GNU text style. From a repository's root directory try:
16
+
17
+ $ vclog
18
+
19
+ To generate an XML formatted changelog use:
20
+
21
+ $ vclog --xml
22
+
23
+ See 'vclog --help' for more options.
24
+
25
+
26
+ == RELEASE NOTES
27
+
28
+ Please see RELEASE file.
29
+
30
+
31
+ == LICENSE & COPYRIGHT
32
+
33
+ VCLog
34
+
35
+ Copyright (c) 2008,2009 TigerOps.org
36
+
37
+ VCLog is distributed under the terms of the GPLv3 license.
38
+
data/RELEASE ADDED
@@ -0,0 +1,11 @@
1
+ = RELEASE NOTES
2
+
3
+ This is the first "production" release of VCLog.
4
+
5
+ ### 1.0.0 / 2009-08-03
6
+
7
+ * 2 Major Enhancements
8
+
9
+ * Improved command line interface.
10
+ * Added output option to save changelog.
11
+
data/bin/vclog ADDED
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # vclog
4
+ #
5
+ # SYNOPSIS
6
+ #
7
+ # VCLog provides cross-vcs ChangeLogs. It works by
8
+ # parsing the native changelog a VCS system produces
9
+ # into a common model, which then can be used to
10
+ # produce Changelogs in a variety of formats.
11
+ #
12
+ # VCLog currently support SVN and Git. CVS, Darcs and
13
+ # Mercurial/Hg are in the works.
14
+ #
15
+ # EXAMPLES
16
+ #
17
+ # To produce a GNU-like changelog:
18
+ #
19
+ # $ vclog
20
+ #
21
+ # For XML format:
22
+ #
23
+ # $ vclog --xml
24
+ #
25
+ # Or for a micorformat-ish HTML:
26
+ #
27
+ # $ vclog --html
28
+ #
29
+ #To use the library programmatically, please see the API documentation.
30
+ #
31
+ # LICENSE
32
+ #
33
+ # VCLog Copyright (c) 2008 Tiger Ops
34
+ # VCLog is distributed under the terms of the GPLv3.
35
+
36
+ require 'vclog/vcs'
37
+ require 'getoptlong'
38
+
39
+ # TODO: rev option.
40
+ #
41
+ def vclog
42
+
43
+ opts = GetoptLong.new(
44
+ [ '--help' , '-h', GetoptLong::NO_ARGUMENT ],
45
+ [ '--debug', GetoptLong::NO_ARGUMENT ],
46
+ [ '--typed', GetoptLong::NO_ARGUMENT ],
47
+ [ '--rev' , GetoptLong::NO_ARGUMENT ],
48
+ [ '--gnu' , GetoptLong::NO_ARGUMENT ],
49
+ [ '--xml' , GetoptLong::NO_ARGUMENT ],
50
+ [ '--html' , GetoptLong::NO_ARGUMENT ],
51
+ [ '--rel' , GetoptLong::REQUIRED_ARGUMENT ],
52
+ [ '--style', GetoptLong::REQUIRED_ARGUMENT ],
53
+ [ '--output', '-o', GetoptLong::REQUIRED_ARGUMENT ]
54
+ )
55
+
56
+ format = :gnu
57
+ typed = false
58
+ rev = false
59
+ vers = nil
60
+ style = nil
61
+ output = nil
62
+
63
+ opts.each do |opt, arg|
64
+ case opt
65
+ when '--help'
66
+ puts "vclog [--gnu|--html|--xml|--rel] [--typed]"
67
+ exit
68
+ when '--debug'
69
+ $DEBUG = true
70
+ when '--typed'
71
+ typed = true
72
+ when '--rev'
73
+ rev = true
74
+ when '--xml'
75
+ format = :xml
76
+ when '--html'
77
+ format = :html
78
+ when '--rel'
79
+ format = :rel
80
+ vers = arg
81
+ when '--style'
82
+ style = arg
83
+ when '--output'
84
+ output = arg
85
+ end
86
+ end
87
+
88
+ vcs = VCLog::VCS.new
89
+
90
+ changelog = vcs.changelog
91
+
92
+ if typed
93
+ changelog = changelog.typed
94
+ end
95
+
96
+ case format
97
+ when :xml
98
+ log = changelog.format_xml(style) # xsl stylesheet url
99
+ when :html
100
+ log = changelog.format_html(style) # css stylesheet url
101
+ when :rel
102
+ if output && File.file?(output)
103
+ file = output
104
+ else
105
+ file = Dir.glob('change{s,log}{,.txt}', File::FNM_CASEFOLD).first
106
+ end
107
+ log = changelog.format_rel(file, vers)
108
+ else #:gnu
109
+ log = changelog.to_s
110
+ end
111
+
112
+ if output
113
+ File.open(output, 'w') do |f|
114
+ f << log
115
+ end
116
+ else
117
+ puts log
118
+ end
119
+
120
+ end
121
+
122
+ begin
123
+ vclog
124
+ rescue => err
125
+ if $DEBUG
126
+ raise err
127
+ else
128
+ puts err.message
129
+ exit -1
130
+ end
131
+ end
@@ -0,0 +1,484 @@
1
+ module VCLog
2
+
3
+ require 'vclog/core_ext'
4
+
5
+ # Supports output formats:
6
+ #
7
+ # xml
8
+ # html
9
+ # yaml
10
+ # json
11
+ # text
12
+ #
13
+ class Changelog
14
+ include Enumerable
15
+
16
+ DAY = 24*60*60
17
+
18
+ attr :changes
19
+
20
+ def initialize(changes=nil)
21
+ @changes = changes || []
22
+ end
23
+
24
+ def change(date, who, rev, note)
25
+ note, type = *split_note(note)
26
+ note = note.gsub(/^\s*?\n/m,'') # remove blank lines
27
+ @changes << Entry.new(:when=>date, :who=>who, :rev=>rev, :type=>type, :msg=>note)
28
+ end
29
+
30
+ def each(&block) ; @changes.each(&block) ; end
31
+ def empty? ; @changes.empty? ; end
32
+ def size ; @changes.size ; end
33
+
34
+ #
35
+ def <<(entry)
36
+ raise unless Entry===entry
37
+ @changes << entry
38
+ end
39
+
40
+ # Return a new changelog with entries that have a specified type.
41
+ # TODO: Be able to specify which types to include or omit.
42
+ def typed
43
+ self.class.new(changes.select{ |e| e.type })
44
+ end
45
+
46
+ # Return a new changelog with entries occuring after the
47
+ # given date limit.
48
+ def after(date_limit)
49
+ after = changes.select{ |entry| entry.date > date_limit + DAY }
50
+ self.class.new(after)
51
+ end
52
+
53
+ # Return a new changelog with entries occuring before the
54
+ # given date limit.
55
+ def before(date_limit)
56
+ before = changes.select{ |entry| entry.date <= date_limit + DAY }
57
+ self.class.new(before)
58
+ end
59
+
60
+ #
61
+
62
+ #
63
+ def by_type
64
+ mapped = {}
65
+ changes.each do |entry|
66
+ mapped[entry.type] ||= self.class.new
67
+ mapped[entry.type] << entry
68
+ end
69
+ mapped
70
+ end
71
+
72
+ #
73
+ def by_author
74
+ mapped = {}
75
+ changes.each do |entry|
76
+ mapped[entry.author] ||= self.class.new
77
+ mapped[entry.author] << entry
78
+ end
79
+ mapped
80
+ end
81
+
82
+ #
83
+ def by_date
84
+ mapped = {}
85
+ changes.each do |entry|
86
+ mapped[entry.date.strftime('%Y-%m-%d')] ||= self.class.new
87
+ mapped[entry.date.strftime('%Y-%m-%d')] << entry
88
+ end
89
+ mapped = mapped.to_a.sort{ |a,b| b[0] <=> a[0] }
90
+ mapped
91
+ end
92
+
93
+ #
94
+ #def by_date
95
+ # mapped = {}
96
+ # changes.each do |entry|
97
+ # mapped[entry.date.strftime('%Y-%m-%d')] ||= self.class.new
98
+ # mapped[entry.date.strftime('%Y-%m-%d')] << entry
99
+ # end
100
+ # mapped
101
+ #end
102
+
103
+ ##################
104
+ # Output Formats #
105
+ ##################
106
+
107
+ def format_yaml
108
+ require 'yaml'
109
+ changes.to_yaml
110
+ end
111
+
112
+ def format_json
113
+ require 'json'
114
+ changes.to_json
115
+ end
116
+
117
+ #
118
+ def format_gnu
119
+ x = []
120
+ by_date.each do |date, date_changes|
121
+ date_changes.by_author.each do |author, author_changes|
122
+ x << %[#{date} #{author}\n]
123
+ #author_changes = author_changes.sort{|a,b| b.date <=> a.date}
124
+ author_changes.each do |entry|
125
+ if entry.type
126
+ msg = "#{entry.message} [#{entry.type}]".tabto(10)
127
+ else
128
+ msg = "#{entry.message}".tabto(10)
129
+ end
130
+ msg[8] = '*'
131
+ x << msg
132
+ end
133
+ end
134
+ x << "\n"
135
+ end
136
+ return x.join("\n")
137
+ end
138
+
139
+ #
140
+ alias_method :to_s, :format_gnu
141
+
142
+ # Create an XML formated changelog.
143
+ # +xsl+ reference defaults to 'log.xsl'
144
+ def format_xml(xsl=nil)
145
+ xsl = 'log.xsl' if xsl.nil?
146
+ x = []
147
+ x << %[<?xml version="1.0"?>]
148
+ x << %[<?xml-stylesheet href="#{xsl}" type="text/xsl" ?>] if xsl
149
+ x << %[<log>]
150
+ changes.sort{|a,b| b.date <=> a.date}.each do |entry|
151
+ x << %[<entry>]
152
+ x << %[ <date>#{entry.date}</date>]
153
+ x << %[ <author>#{escxml(entry.author)}</author>]
154
+ x << %[ <revison>#{escxml(entry.revison)}</revison>]
155
+ x << %[ <type>#{escxml(entry.type)}</type>]
156
+ x << %[ <message>#{escxml(entry.message)}</message>]
157
+ x << %[</entry>]
158
+ end
159
+ x << %[</log>]
160
+ return x.join("\n")
161
+ end
162
+
163
+ # Create an HTML formated changelog.
164
+ # +css+ reference defaults to 'log.css'
165
+ def format_html(css=nil)
166
+ css = 'log.css' if css.nil?
167
+ x = []
168
+ x << %[<html>]
169
+ x << %[<head>]
170
+ x << %[ <title>Changelog</title>]
171
+ x << %[ <style>]
172
+ x << %[ body{font-family: sans-serif;}]
173
+ x << %[ #changelog{width:800px;margin:0 auto;}]
174
+ x << %[ li{padding: 10px;}]
175
+ x << %[ .date{font-weight: bold; color: gray; float: left; padding: 0 5px;}]
176
+ x << %[ .author{color: red;}]
177
+ x << %[ .message{padding: 5 0; font-weight: bold;}]
178
+ x << %[ .revison{font-size: 0.8em;}]
179
+ x << %[ </style>]
180
+ x << %[ <link rel="stylesheet" href="#{css}" type="text/css">] if css
181
+ x << %[</head>]
182
+ x << %[<body>]
183
+ x << %[ <div id="changelog">]
184
+ x << %[ <h1>ChangeLog</h1>]
185
+ x << %[ <ul class="log">]
186
+ changes.sort{|a,b| b.date <=> a.date}.each do |entry|
187
+ x << %[ <li class="entry">]
188
+ x << %[ <div class="date">#{entry.date}</div>]
189
+ x << %[ <div class="author">#{escxml(entry.author)}</div>]
190
+ x << %[ <div class="type">#{escxml(entry.type)}</div>]
191
+ x << %[ <div class="message">#{escxml(entry.message)}</div>]
192
+ x << %[ <div class="revison">##{escxml(entry.revison)}</div>]
193
+ x << %[ </li>]
194
+ end
195
+ x << %[ </ul>]
196
+ x << %[ </div>]
197
+ x << %[</body>]
198
+ x << %[</html>]
199
+ return x.join("\n")
200
+ end
201
+
202
+ #
203
+ def format_rel(file, current_version=nil, current_release=nil)
204
+ log = []
205
+ # collect releases already listed in changelog file
206
+ rels = releases(file)
207
+ # add current verion to release list (if given)
208
+ previous_version = rels[0][0]
209
+ if current_version < previous_version
210
+ raise ArgumentError, "Release version is less than previous version (#{previous_version})."
211
+ end
212
+ #case current_version
213
+ #when 'major'
214
+ # v = previous_verison.split(/\W/)
215
+ # v[0] = v[0].succ
216
+ # current_version = v.join('.')
217
+ #when 'minor'
218
+ # v = previous_verison.split(/\W/)
219
+ # v[1] = v[1].succ
220
+ # current_version = v.join('.')
221
+ # end
222
+ #end
223
+ rels << [current_version, current_release || Time.now]
224
+ # make sure all release date are Time objects
225
+ rels = rels.collect{ |v,d| [v, Time===d ? d : Time.parse(d)] }
226
+ # only uniq releases
227
+ rels = rels.uniq
228
+ # sort by release date
229
+ rels = rels.to_a.sort{ |a,b| a[1] <=> b[1] }
230
+ # organize into deltas
231
+ deltas, last = [], nil
232
+ rels.each do |rel|
233
+ deltas << [last, rel]
234
+ last = rel
235
+ end
236
+ # gather changes for each delta and build log
237
+ deltas.each do |gt, lt|
238
+ if gt
239
+ gt_vers, gt_date = *gt
240
+ lt_vers, lt_date = *lt
241
+ #gt_date = Time.parse(gt_date) unless Time===gt_date
242
+ #lt_date = Time.parse(lt_date) unless Time===lt_date
243
+ changes = after(gt_date).before(lt_date)
244
+ else
245
+ lt_vers, lt_date = *lt
246
+ #lt_date = Time.parse(lt_date) unless Time===lt_date
247
+ changes = before(lt_date)
248
+ end
249
+ reltext = changes.format_rel_types
250
+ unless reltext.strip.empty?
251
+ log << "== #{lt_vers} / #{lt_date.strftime('%Y-%m-%d')}\n\n#{reltext}"
252
+ end
253
+ end
254
+ # reverse log order and make into document
255
+ log.reverse.join("\n")
256
+ end
257
+
258
+ #
259
+ def format_rel_types
260
+ groups = changes.group_by{ |e| e.type_number }
261
+ string = ""
262
+ 5.times do |n|
263
+ entries = groups[n]
264
+ next if !entries
265
+ next if entries.empty?
266
+ string << "* #{entries.size} #{entries[0].type_phrase}\n\n"
267
+ entries.sort!{|a,b| a.date <=> b.date }
268
+ entries.each do |entry|
269
+ #string << "== #{date} #{who}\n\n" # no email :(
270
+ text = "#{entry.message} (##{entry.revison})"
271
+ text = text.tabto(6)
272
+ text[4] = '*'
273
+ #entry = entry.join(' ').tabto(6)
274
+ #entry[4] = '*'
275
+ string << text
276
+ string << "\n"
277
+ end
278
+ string << "\n"
279
+ end
280
+ string
281
+ end
282
+
283
+ #
284
+ def releases(file)
285
+ return [] unless file
286
+ clog = File.read(file)
287
+ tags = clog.scan(/^==(.*?)$/)
288
+ rels = tags.collect do |t|
289
+ parse_version_tag(t[0])
290
+ end
291
+ return rels
292
+ end
293
+
294
+ #
295
+ def parse_version_tag(tag)
296
+ version, date = *tag.split('/')
297
+ version, date = version.sub(/^\=+\s*/,'').strip, date.strip
298
+ return version, date
299
+ end
300
+
301
+ ###################
302
+ # Save Chaqngelog #
303
+ ###################
304
+
305
+ # Save changelog as file in specified format.
306
+ def save(file, format=:gnu, *args)
307
+ case format.to_sym
308
+ when :xml
309
+ text = format_xml
310
+ save_xsl(file)
311
+ when :html
312
+ text = format_html(*args)
313
+ when :rel
314
+ text = format_rel(file, *args)
315
+ when :yaml
316
+ text = format_yaml(file)
317
+ when :json
318
+ text = format_json(file)
319
+ else
320
+ text = format_gnu
321
+ end
322
+
323
+ FileUtils.mkdir_p(File.dirname(file))
324
+
325
+ different = true
326
+ if File.exist?(file)
327
+ different = (File.read(file) != text)
328
+ end
329
+
330
+ File.open(file, 'w') do |f|
331
+ f << text
332
+ end if different
333
+ end
334
+
335
+ private
336
+
337
+ #
338
+ def escxml(input)
339
+ result = input.to_s.dup
340
+ result.gsub!("&", "&amp;")
341
+ result.gsub!("<", "&lt;")
342
+ result.gsub!(">", "&gt;")
343
+ result.gsub!("'", "&apos;")
344
+ #result.gsub!("@", "&at;")
345
+ result.gsub!("\"", "&quot;")
346
+ return result
347
+ end
348
+
349
+ #
350
+ def save_xsl(file)
351
+ #xslfile = file.chomp(File.extname(file)) + '.xsl'
352
+ xslfile = File.join(File.dirname(file), 'log.xsl')
353
+ unless File.exist?(xslfile)
354
+ FileUtils.mkdir_p(File.dirname(xslfile))
355
+ File.open(xslfile, 'w') do |f|
356
+ f << DEFAULT_LOG_XSL
357
+ end
358
+ end
359
+ end
360
+
361
+ #
362
+ def split_note(note)
363
+ note = note.strip
364
+ if md = /\A.*?\[(.*?)\]\s*$/.match(note)
365
+ t = md[1].strip.downcase
366
+ n = note.sub(/\[#{md[1]}\]\s*$/, "")
367
+ else
368
+ n, t = note, nil
369
+ end
370
+ return n, t
371
+ end
372
+
373
+ # = Change Log Entry class
374
+ class Entry
375
+ include Comparable
376
+
377
+ attr_accessor :author
378
+ attr_accessor :date
379
+ attr_accessor :revison
380
+ attr_accessor :message
381
+ attr_accessor :type
382
+
383
+ def initialize(opts={})
384
+ @author = opts[:author] || opts[:who]
385
+ @date = opts[:date] || opts[:when]
386
+ @revison = opts[:revison] || opts[:rev]
387
+ @message = opts[:message] || opts[:msg]
388
+ @type = opts[:type]
389
+ end
390
+
391
+ #def clean_type(type)
392
+ # case type.to_s
393
+ # when 'maj', 'major' then :major
394
+ # when 'min', 'minor' then :minor
395
+ # when 'bug' then :bug
396
+ # when '' then :other
397
+ # else
398
+ # type.to_sym
399
+ # end
400
+ #end
401
+
402
+ #
403
+ def type_phrase
404
+ case type.to_s
405
+ when 'maj', 'major'
406
+ 'Major Enhancements'
407
+ when 'min', 'minor'
408
+ 'Minor Enhancements'
409
+ when 'bug'
410
+ 'Bug Fixes'
411
+ when ''
412
+ 'Further Enhancements'
413
+ else
414
+ "#{type.capitalize} Enhancements"
415
+ end
416
+ end
417
+
418
+ #
419
+ def type_number
420
+ case type.to_s.downcase
421
+ when 'maj', 'major'
422
+ 0
423
+ when 'min', 'minor'
424
+ 1
425
+ when 'bug'
426
+ 2
427
+ when ''
428
+ 4
429
+ else # other
430
+ 3
431
+ end
432
+ end
433
+
434
+ #
435
+ def <=>(other)
436
+ other.date <=> date
437
+ end
438
+
439
+ def inspect
440
+ "#<Entry:#{object_id} #{date}>"
441
+ end
442
+
443
+ end #class Entry
444
+
445
+ end
446
+
447
+ DEFAULT_LOG_XSL = <<-END.tabto(0)
448
+ <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
449
+
450
+ <xsl:output cdata-section-elements="script"/>
451
+
452
+ <xsl:template match="/">
453
+ <html>
454
+ <head>
455
+ <title>Changelog</title>
456
+ <link REL='SHORTCUT ICON' HREF="../img/ruby-sm.png" />
457
+ <style>
458
+ td { font-family: sans-serif; padding: 0px 10px; }
459
+ </style>
460
+ </head>
461
+ <body>
462
+ <div class="container">
463
+ <h1>Changelog</h1>
464
+ <table style="width: 100%;">
465
+ <xsl:apply-templates />
466
+ </table>
467
+ </div>
468
+ </body>
469
+ </html>
470
+ </xsl:template>
471
+
472
+ <xsl:template match="entry">
473
+ <tr>
474
+ <td><b><pre><xsl:value-of select="message"/></pre></b></td>
475
+ <td><xsl:value-of select="author"/></td>
476
+ <td><xsl:value-of select="date"/></td>
477
+ </tr>
478
+ </xsl:template>
479
+
480
+ </xsl:stylesheet>
481
+ END
482
+
483
+ end
484
+