vclog 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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
+