vclog 1.1 → 1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.md +20 -0
- data/MANIFEST +5 -1
- data/TODO +12 -0
- data/bin/vclog +1 -1
- data/lib/vclog/change.rb +108 -0
- data/lib/vclog/changelog.rb +136 -146
- data/lib/vclog/cli.rb +220 -0
- data/lib/vclog/history.rb +314 -0
- data/lib/vclog/release.rb +23 -0
- data/lib/vclog/tag.rb +54 -0
- data/lib/vclog/vcs.rb +93 -16
- data/lib/vclog/vcs/git.rb +52 -21
- data/lib/vclog/vcs/svn.rb +66 -18
- data/meta/version +1 -1
- metadata +7 -3
- data/lib/vclog/command.rb +0 -168
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
|
+
|