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/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
|
+
|