vclog 1.1 → 1.2

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