vclog 1.1 → 1.2

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/vclog/cli.rb ADDED
@@ -0,0 +1,220 @@
1
+ module VCLog
2
+
3
+ require 'vclog/vcs'
4
+ require 'optparse'
5
+
6
+ # = vclog Command
7
+ #
8
+ # == SYNOPSIS
9
+ #
10
+ # VCLog provides cross-vcs ChangeLogs. It works by
11
+ # parsing the native changelog a VCS system produces
12
+ # into a common model, which then can be used to
13
+ # produce Changelogs in a variety of formats.
14
+ #
15
+ # VCLog currently support SVN and Git. CVS, Darcs and
16
+ # Mercurial/Hg are in the works.
17
+ #
18
+ # == EXAMPLES
19
+ #
20
+ # To produce a GNU-like changelog:
21
+ #
22
+ # $ vclog
23
+ #
24
+ # For XML format:
25
+ #
26
+ # $ vclog --xml
27
+ #
28
+ # Or for a micorformat-ish HTML:
29
+ #
30
+ # $ vclog --html
31
+ #
32
+ # To use the library programmatically, please see the API documentation.
33
+
34
+ def self.run
35
+ begin
36
+ vclog
37
+ rescue => err
38
+ if $DEBUG
39
+ raise err
40
+ else
41
+ puts err.message
42
+ exit -1
43
+ end
44
+ end
45
+ end
46
+
47
+ #
48
+ def self.vclog
49
+ type = :log
50
+ format = :gnu
51
+ vers = nil
52
+ style = nil
53
+ output = nil
54
+ title = nil
55
+ version = nil
56
+ extra = false
57
+ rev = false
58
+ typed = false
59
+
60
+ optparse = OptionParser.new do |opt|
61
+
62
+ opt.banner = "Usage: vclog [TYPE] [FORMAT] [OPTIONS] [DIR]"
63
+
64
+ opt.separator(" ")
65
+ opt.separator("OUTPUT TYPE (choose one):")
66
+
67
+ opt.on('--log', '--changelog', '-l', "changelog (default)") do
68
+ type = :log
69
+ end
70
+
71
+ opt.on('--rel', '--history', '-r', "release history") do
72
+ type = :rel
73
+ end
74
+
75
+ opt.on('--bump', '-b', "display a bumped version number") do
76
+ doctype = :bump
77
+ end
78
+
79
+ opt.on('--current', '-c', "display current version number") do
80
+ doctype = :curr
81
+ end
82
+
83
+ opt.separator(" ")
84
+ opt.separator("FORMAT (choose one):")
85
+
86
+ opt.on('--gnu', "GNU standard format (default)") do
87
+ format = :gnu
88
+ end
89
+
90
+ opt.on('--xml', "XML format") do
91
+ format = :xml
92
+ end
93
+
94
+ opt.on('--yaml', "YAML format") do
95
+ format = :yaml
96
+ end
97
+
98
+ opt.on('--json', "JSON format") do
99
+ format = :json
100
+ end
101
+
102
+ opt.on('--html', "HTML micro-like format") do
103
+ format = :html
104
+ end
105
+
106
+ opt.on('--rdoc', "RDoc format") do
107
+ format = :rdoc
108
+ end
109
+
110
+ opt.on('--markdown', '-m', "Markdown format") do
111
+ format = :markdown
112
+ end
113
+
114
+ opt.separator(" ")
115
+ opt.separator("OTHER OPTIONS:")
116
+
117
+ #opt.on('--typed', "catagorize by commit type") do
118
+ # typed = true
119
+ #end
120
+
121
+ opt.on('--title <TITLE>', "document title, used by some formats") do |string|
122
+ title = string
123
+ end
124
+
125
+ opt.on('--extra', '-e', "provide extra output, used by some formats") do
126
+ extra = true
127
+ end
128
+
129
+ opt.on('--version', '-v <NUM>', "current version to use for release history") do |num|
130
+ version = num
131
+ end
132
+
133
+ opt.on('--style [FILE]', "provide a stylesheet name (css or xsl) for xml and html formats") do |val|
134
+ style = val
135
+ end
136
+
137
+ opt.on('--id', "include revision ids (in formats that normally do not)") do
138
+ rev = true
139
+ end
140
+
141
+ # DEPRECATE
142
+ opt.on('--output', '-o [FILE]', "send output to a file instead of stdout") do |out|
143
+ output = out
144
+ end
145
+
146
+ opt.separator(" ")
147
+ opt.separator("STANDARD OPTIONS:")
148
+
149
+ opt.on('--debug', "show debugging infromation") do
150
+ $DEBUG = true
151
+ end
152
+
153
+ opt.on_tail('--help' , '-h', 'display this help information') do
154
+ puts opt
155
+ exit
156
+ end
157
+ end
158
+
159
+ optparse.parse!(ARGV)
160
+
161
+ root = ARGV.shift || Dir.pwd
162
+
163
+ vcs = VCLog::VCS.factory #(root)
164
+
165
+ case type
166
+ when :bump
167
+ puts vcs.bump(version)
168
+ exit
169
+ when :curr
170
+ puts vcs.tags.last.name #TODO: ensure latest
171
+ exit
172
+ when :log
173
+ log = vcs.changelog
174
+ #log = log.typed if typed #TODO: ability to select types?
175
+ when :rel
176
+ log = vcs.history(:title=>title, :extra=>extra, :version=>version)
177
+ else
178
+ raise "huh?"
179
+ #log = vcs.changelog
180
+ #log = log.typed if typed #TODO: ability to select types?
181
+ end
182
+
183
+ case format
184
+ when :xml
185
+ txt = log.to_xml(style) # xsl stylesheet url
186
+ when :html
187
+ txt = log.to_html(style) # css stylesheet url
188
+ when :yaml
189
+ txt = log.to_yaml
190
+ when :json
191
+ txt = log.to_json
192
+ when :markdown
193
+ txt = log.to_markdown(rev)
194
+ when :rdoc
195
+ txt = log.to_rdoc(rev)
196
+ else #:gnu
197
+ txt = log.to_gnu(rev)
198
+ end
199
+
200
+ if output
201
+ File.open(output, 'w') do |f|
202
+ f << txt
203
+ end
204
+ else
205
+ puts txt
206
+ end
207
+ end
208
+
209
+ #def self.changelog_file(file)
210
+ # if file && File.file?(file)
211
+ # file
212
+ # else
213
+ # Dir.glob('{history,changes,changelog}{,.*}', File::FNM_CASEFOLD).first
214
+ # end
215
+ #end
216
+
217
+ end
218
+
219
+ # VCLog Copyright (c) 2008 Thomas Sawyer
220
+
@@ -0,0 +1,314 @@
1
+ module VCLog
2
+
3
+ require 'vclog/facets'
4
+ require 'vclog/changelog'
5
+ require 'vclog/tag'
6
+ require 'vclog/release'
7
+
8
+ # = Release History Class
9
+ #
10
+ # A Release History is very similar to a ChangeLog.
11
+ # It differs in that it is divided into releases with
12
+ # version, release date and release note.
13
+ #
14
+ # The release version, date and release note can be
15
+ # descerened from the underlying SCM by identifying
16
+ # the hard tag commits.
17
+ #
18
+ # But we can also extract the release information from a
19
+ # release history file, if provided. And it's release
20
+ # information should take precidence. ???
21
+ #
22
+ #
23
+ # TODO: Extract output formating from delta parser.
24
+ #
25
+ class History
26
+
27
+ attr :vcs
28
+
29
+ attr_accessor :marker
30
+
31
+ attr_accessor :title
32
+
33
+ # Current working version.
34
+ attr_accessor :version
35
+
36
+ attr_accessor :extra
37
+
38
+ #
39
+ def initialize(vcs, opts={})
40
+ @vcs = vcs
41
+ @marker = opts[:marker] || "#"
42
+ @title = opts[:title] || "RELEASE HISTORY"
43
+ @extra = opts[:extra]
44
+ @version = opts[:version]
45
+ end
46
+
47
+ # Tag list from version control system.
48
+ def tags
49
+ @tags ||= vcs.tags
50
+ end
51
+
52
+ # Change list from version control system.
53
+ def changes
54
+ @changes ||= vcs.changes
55
+ end
56
+
57
+ # Changelog object
58
+ def changelog
59
+ @changlog ||= vcs.changelog #ChangeLog.new(changes)
60
+ end
61
+
62
+ #
63
+ def releases
64
+ @releases ||= (
65
+ rel = []
66
+
67
+ tags = tags()
68
+
69
+ ver = vcs.bump(version)
70
+ time = ::Time.now
71
+ user = ENV['USER'] # TODO: get user name from vcs
72
+
73
+ tags << Tag.new(ver, time, user, "FIXME")
74
+
75
+ # TODO: Do we need to add a Time.now tag?
76
+ # add current verion to release list (if given)
77
+ #previous_version = tags[0].name
78
+ #if current_version < previous_version # TODO: need to use natural comparision
79
+ # raise ArgumentError, "Release version is less than previous version (#{previous_version})."
80
+ #end
81
+ #rels << [current_version, current_release || Time.now]
82
+ #rels = rels.uniq # only uniq releases
83
+
84
+ # sort by release date
85
+ tags = tags.sort{ |a,b| a.date <=> b.date }
86
+
87
+ # organize into deltas
88
+ deltas, last = [], nil
89
+ tags.each do |tag|
90
+ deltas << [last, tag]
91
+ last = tag
92
+ end
93
+
94
+ # gather changes for each delta and build log
95
+ deltas.each do |gt, lt|
96
+ if gt
97
+ gt_vers, gt_date = gt.name, gt.date
98
+ lt_vers, lt_date = lt.name, lt.date
99
+ #gt_date = Time.parse(gt_date) unless Time===gt_date
100
+ #lt_date = Time.parse(lt_date) unless Time===lt_date
101
+ log = changelog.after(gt_date).before(lt_date)
102
+ else
103
+ lt_vers, lt_date = lt.name, lt.date
104
+ #lt_date = Time.parse(lt_date) unless Time===lt_date
105
+ log = changelog.before(lt_date)
106
+ end
107
+
108
+ rel << Release.new(lt, log.changes)
109
+ end
110
+ rel
111
+ )
112
+ end
113
+
114
+ #
115
+ def to_s(rev=false)
116
+ to_gnu(rev)
117
+ end
118
+
119
+ # TODO: What would GNU history be?
120
+ def to_gnu(rev=false)
121
+ to_markdown(rev)
122
+ end
123
+
124
+ # Translate history into an XML document.
125
+ def to_xml(xsl=nil)
126
+ require 'rexml/document'
127
+ xml = REXML::Document.new('<history></history>')
128
+ #xml << REXML::XMLDecl.default
129
+ root = xml.root
130
+ releases.each do |release|
131
+ rel = root.add_element('release')
132
+ tel = rel.add_element('tag')
133
+ tel.add_element('name').add_text(release.tag.name)
134
+ tel.add_element('date').add_text(release.tag.date.to_s)
135
+ tel.add_element('author').add_text(release.tag.author)
136
+ tel.add_element('message').add_text(release.tag.message)
137
+ cel = rel.add_element('changes')
138
+ release.changes.sort{|a,b| b.date <=> a.date}.each do |entry|
139
+ el = cel.add_element('entry')
140
+ el.add_element('date').add_text(entry.date.to_s)
141
+ el.add_element('author').add_text(entry.author)
142
+ el.add_element('type').add_text(entry.type)
143
+ el.add_element('revision').add_text(entry.revision)
144
+ el.add_element('message').add_text(entry.message)
145
+ end
146
+ end
147
+ out = String.new
148
+ fmt = REXML::Formatters::Pretty.new
149
+ fmt.compact = true
150
+ fmt.write(xml, out)
151
+ #
152
+ txt = %[<?xml version="1.0"?>\n]
153
+ txt += %[<?xml-stylesheet href="#{xsl}" type="text/xsl" ?>\n] if xsl
154
+ txt += out
155
+ txt
156
+ end
157
+
158
+ # Translate history into a HTML document.
159
+ #
160
+ # TODO: Need to add some headers.
161
+ #
162
+ def to_html(css=nil)
163
+ require 'rexml/document'
164
+ xml = REXML::Document.new('<div class="history"></div>')
165
+ #xml << REXML::XMLDecl.default
166
+ root = xml.root
167
+ releases.each do |release|
168
+ rel = root.add_element('div')
169
+ rel.add_attribute('class', 'release')
170
+ tel = rel.add_element('div')
171
+ tel.add_attribute('class', 'tag')
172
+ tel.add_element('div').add_text(release.tag.name).add_attribute('class', 'name')
173
+ tel.add_element('div').add_text(release.tag.date.to_s).add_attribute('class', 'date')
174
+ tel.add_element('div').add_text(release.tag.author).add_attribute('class', 'author')
175
+ tel.add_element('div').add_text(release.tag.message).add_attribute('class', 'message')
176
+ cel = rel.add_element('ul')
177
+ cel.add_attribute('class', 'changes')
178
+ release.changes.sort{|a,b| b.date <=> a.date}.each do |entry|
179
+ el = cel.add_element('li')
180
+ el.add_attribute('class', 'entry')
181
+ el.add_element('div').add_text(entry.date.to_s).add_attribute('class', 'date')
182
+ el.add_element('div').add_text(entry.author).add_attribute('class', 'author')
183
+ el.add_element('div').add_text(entry.type).add_attribute('class', 'type')
184
+ el.add_element('div').add_text(entry.revision).add_attribute('class', 'revision')
185
+ el.add_element('div').add_text(entry.message).add_attribute('class', 'message')
186
+ end
187
+ end
188
+ out = String.new
189
+ fmt = REXML::Formatters::Pretty.new
190
+ fmt.compact = true
191
+ fmt.write(xml, out)
192
+ #
193
+ x = []
194
+ x << %[<html>]
195
+ x << %[<head>]
196
+ x << %[ <title>ChangeLog</title>]
197
+ x << %[ <style>]
198
+ x << %[ body{font-family: sans-serif;}]
199
+ x << %[ #changelog{width:800px;margin:0 auto;}]
200
+ x << %[ li{padding: 10px;}]
201
+ x << %[ .date{font-weight: bold; color: gray; float: left; padding: 0 5px;}]
202
+ x << %[ .author{color: red;}]
203
+ x << %[ .message{padding: 5 0; font-weight: bold;}]
204
+ x << %[ .revision{font-size: 0.8em;}]
205
+ x << %[ </style>]
206
+ x << %[ <link rel="stylesheet" href="#{css}" type="text/css">] if css
207
+ x << %[</head>]
208
+ x << %[<body>]
209
+ x << out
210
+ x << %[</body>]
211
+ x << %[</html>]
212
+ x.join("\n")
213
+ end
214
+
215
+ # Translate history into a YAML document.
216
+ def to_yaml(*args)
217
+ require 'yaml'
218
+ releases.to_yaml(*args)
219
+ end
220
+
221
+ # Translate history into a JSON document.
222
+ def to_json
223
+ require 'json'
224
+ releases.to_json
225
+ end
226
+
227
+ # Translate history into a Markdown formatted document.
228
+ def to_markdown(rev=false)
229
+ to_markup('#', rev)
230
+ end
231
+
232
+ # Translate history into a RDoc formatted document.
233
+ def to_rdoc(rev=false)
234
+ to_markup('=', rev)
235
+ end
236
+
237
+ #
238
+ def to_markup(marker, rev=false)
239
+ entries = []
240
+ releases.each do |release|
241
+ tag = release.tag
242
+ changes = release.changes
243
+ change_text = to_markup_changes(changes, rev)
244
+ unless change_text.strip.empty?
245
+ if extra
246
+ entries << "#{marker*2} #{tag.name} / #{tag.date.strftime('%Y-%m-%d')}\n\n#{tag.message}\n\nChanges:\n\n#{change_text}"
247
+ else
248
+ entries << "#{marker*2} #{tag.name} / #{tag.date.strftime('%Y-%m-%d')}\n\n#{change_text}"
249
+ end
250
+ end
251
+ end
252
+ # reverse entries order and make into document
253
+ marker + " #{title}\n\n" + entries.reverse.join("\n")
254
+ end
255
+
256
+ private
257
+
258
+ #
259
+ def to_markup_changes(changes, rev=false)
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
+ if rev
271
+ text = "#{entry.message} (##{entry.revision})"
272
+ else
273
+ text = "#{entry.message}"
274
+ end
275
+ text = text.tabto(6)
276
+ text[4] = '*'
277
+ #entry = entry.join(' ').tabto(6)
278
+ #entry[4] = '*'
279
+ string << text
280
+ string << "\n"
281
+ end
282
+ string << "\n"
283
+ end
284
+ string
285
+ end
286
+
287
+ =begin
288
+ # Extract release tags from a release file.
289
+ #
290
+ # TODO: need to extract message (?)
291
+ #
292
+ def releases_from_file(file)
293
+ return [] unless file
294
+ clog = File.read(file)
295
+ tags = clog.scan(/^(==|##)(.*?)$/)
296
+ rels = tags.collect do |t|
297
+ parse_version_tag(t[1])
298
+ end
299
+ @marker = tags[0][0]
300
+ return rels
301
+ end
302
+
303
+ #
304
+ def parse_version_tag(tag)
305
+ version, date = *tag.split('/')
306
+ version, date = version.strip, date.strip
307
+ return version, date
308
+ end
309
+ =end
310
+
311
+ end
312
+
313
+ end
314
+