torque 0.3.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +79 -3
- data/VERSION +2 -2
- data/bin/config +24 -13
- data/bin/email +10 -20
- data/bin/format +170 -0
- data/bin/project +14 -23
- data/bin/torque +27 -21
- data/lib/torque.rb +20 -34
- data/lib/torque/date_settings.rb +2 -2
- data/lib/torque/field_filter.rb +8 -9
- data/lib/torque/file_system.rb +6 -6
- data/lib/torque/format_string.rb +107 -0
- data/lib/torque/iteration.rb +1 -1
- data/lib/torque/mailer.rb +0 -5
- data/lib/torque/pivotal.rb +8 -8
- data/lib/torque/pivotal_html_parser.rb +2 -2
- data/lib/torque/project/project.rb +4 -0
- data/lib/torque/project/project_manager.rb +4 -5
- data/lib/torque/record_pathname_settings.rb +1 -1
- data/lib/torque/settings.rb +19 -11
- data/lib/torque/story.rb +32 -21
- data/lib/torque/torque_info_parser.rb +20 -8
- data/lib/torque/version.rb +1 -1
- metadata +4 -2
data/bin/project
CHANGED
@@ -51,7 +51,7 @@ begin
|
|
51
51
|
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
52
52
|
puts $!.to_s
|
53
53
|
puts option_parser
|
54
|
-
exit
|
54
|
+
exit 3
|
55
55
|
end
|
56
56
|
|
57
57
|
# Handles special cases and exceptions
|
@@ -67,7 +67,7 @@ end
|
|
67
67
|
if !Torque::Pivotal.connection?
|
68
68
|
puts "ABORTING"
|
69
69
|
puts "Cannot connect to www.pivotaltracker.com. A connection is required to use Torque"
|
70
|
-
exit
|
70
|
+
exit 4
|
71
71
|
end
|
72
72
|
|
73
73
|
# Locates project identifiers
|
@@ -81,35 +81,25 @@ if options[:name]
|
|
81
81
|
project_identifiers << options[:name]
|
82
82
|
end
|
83
83
|
|
84
|
+
project_manager = Torque::ProjectManager.new
|
85
|
+
begin
|
86
|
+
project_manager.load_project_list
|
87
|
+
rescue Torque::MissingTorqueInfoFileError => e
|
88
|
+
puts "ABORTING"
|
89
|
+
puts e.message
|
90
|
+
puts "Run 'torque config' in this directory, or change your working directory"
|
91
|
+
exit 5
|
92
|
+
end
|
93
|
+
|
84
94
|
if project_identifiers.size == 0
|
85
|
-
|
86
|
-
project_manager = Torque::ProjectManager.new
|
87
|
-
begin
|
88
|
-
project_manager.load_project_list
|
89
|
-
rescue Torque::MissingTorqueInfoFileError => e
|
90
|
-
puts "ABORTING"
|
91
|
-
puts e.message
|
92
|
-
puts "Run 'torque config' in this directory, or change your working directory"
|
93
|
-
exit 2
|
94
|
-
end
|
95
95
|
|
96
96
|
puts project_manager.format_project_list
|
97
97
|
|
98
98
|
elsif project_identifiers.size == 1
|
99
99
|
|
100
|
-
# Throw error if cannot find a matching project
|
101
|
-
if !Pathname.new("./.torqueinfo.yaml").exist?
|
102
|
-
puts "Directory is not configured for torque (.torqueinfo.yaml file is missing). Run 'torque config' in this " \
|
103
|
-
+ "directory, or change your working directory"
|
104
|
-
exit
|
105
|
-
end
|
106
|
-
|
107
100
|
# Switch to a new project
|
108
101
|
identifier = project_identifiers[0]
|
109
102
|
|
110
|
-
project_manager = Torque::ProjectManager.new
|
111
|
-
project_manager.load_project_list
|
112
|
-
|
113
103
|
if is_name
|
114
104
|
project = project_manager.project_list.select { |project| project.name == identifier }[0]
|
115
105
|
else
|
@@ -124,7 +114,7 @@ elsif project_identifiers.size == 1
|
|
124
114
|
end
|
125
115
|
puts
|
126
116
|
puts project_manager.format_project_list
|
127
|
-
exit
|
117
|
+
exit 6
|
128
118
|
end
|
129
119
|
|
130
120
|
Torque::TorqueInfoParser.new.set("project", project.id)
|
@@ -133,4 +123,5 @@ elsif project_identifiers.size == 1
|
|
133
123
|
else
|
134
124
|
# Too many project identifiers. Throw error
|
135
125
|
puts "Only 1 project allowed, found #{project_identifiers.size}: #{project_identifiers.to_s}"
|
126
|
+
exit 3
|
136
127
|
end
|
data/bin/torque
CHANGED
@@ -35,11 +35,14 @@ option_parser = OptionParser.new do |opts|
|
|
35
35
|
opts.banner += "\nSpecial commands:"
|
36
36
|
opts.banner += "\n config Configures a directory for use with Torque"
|
37
37
|
opts.banner += "\n email Sets up user email, adds to/removes from Torque's mailing list"
|
38
|
+
opts.banner += "\n format Controls the format of the generated documents"
|
38
39
|
opts.banner += "\n project Displays & switches between available Pivotal Tracker projects"
|
39
40
|
opts.banner += "\n"
|
40
41
|
opts.banner += "\nOptions:"
|
41
42
|
opts.banner += "\n"
|
42
43
|
|
44
|
+
# TODO --dry and --less options
|
45
|
+
|
43
46
|
opts.on("--email", "Email the compiled notes to the email list in .torqueinfo") do
|
44
47
|
|arg|
|
45
48
|
options[:email] = arg
|
@@ -107,6 +110,9 @@ if(ARGV[0]=="config")
|
|
107
110
|
elsif(ARGV[0]=="email")
|
108
111
|
exec("#{exec_dir}/email #{arg_string}")
|
109
112
|
|
113
|
+
elsif(ARGV[0]=="format")
|
114
|
+
exec("#{exec_dir}/format #{arg_string}")
|
115
|
+
|
110
116
|
elsif(ARGV[0]=="project")
|
111
117
|
exec("#{exec_dir}/project #{arg_string}")
|
112
118
|
|
@@ -125,23 +131,22 @@ begin
|
|
125
131
|
elsif !ARGV.empty?
|
126
132
|
puts "Unknown arguments: #{ARGV.join(", ")}"
|
127
133
|
puts option_parser.help
|
128
|
-
exit
|
134
|
+
exit 3
|
129
135
|
|
130
136
|
elsif (options.has_key?(:accept_to) || options.has_key?(:accept_from)) && options.has_key?(:iterations)
|
131
137
|
err_str = "Conflicting options: "
|
132
138
|
err_str += "-t/--to, " if options.has_key? :accept_to
|
133
139
|
err_str += "-f/--from, " if options.has_key? :accept_from
|
134
|
-
err_str += "-i
|
140
|
+
err_str += "-i/--iterations. "
|
135
141
|
err_str += "Cannot use a custom date range with 'iterations' option"
|
136
142
|
puts err_str
|
137
|
-
|
138
|
-
exit 1
|
143
|
+
exit 3
|
139
144
|
end
|
140
145
|
|
141
146
|
rescue OptionParser::InvalidOption, OptionParser::MissingArgument
|
142
147
|
puts $!.to_s
|
143
148
|
puts option_parser.help
|
144
|
-
exit
|
149
|
+
exit 3
|
145
150
|
end
|
146
151
|
|
147
152
|
# Checks for a connection to Pivotal Tracker
|
@@ -149,39 +154,40 @@ end
|
|
149
154
|
if !Torque::Pivotal.connection?
|
150
155
|
puts "ABORTING"
|
151
156
|
puts "Cannot connect to www.pivotaltracker.com. A connection is required to use Torque"
|
152
|
-
exit
|
157
|
+
exit 4
|
153
158
|
end
|
154
159
|
|
155
160
|
# Runs Torque, handling errors
|
156
161
|
|
157
162
|
begin
|
158
163
|
Torque.new(options).execute
|
164
|
+
rescue Torque::MissingTorqueInfoFileError => e
|
165
|
+
puts "ABORTING"
|
166
|
+
puts e.message
|
167
|
+
puts "Run 'torque config' in this directory, or change your working directory"
|
168
|
+
exit 5
|
169
|
+
rescue Torque::MissingOutputDirectoryError => e
|
170
|
+
puts "ABORTING"
|
171
|
+
puts e.message
|
172
|
+
puts "Run 'torque config -o [directory]' to create/change your output directory"
|
173
|
+
exit 6
|
159
174
|
rescue Torque::MissingTokenError, Torque::InvalidTokenError => e
|
160
175
|
puts "ABORTING"
|
161
176
|
puts e.message
|
162
177
|
puts "Run 'torque config --token' to set your Pivotal Tracker API token"
|
163
|
-
(e.is_a? Torque::MissingTokenError) ? (exit
|
178
|
+
(e.is_a? Torque::MissingTokenError) ? (exit 7) : (exit 8)
|
164
179
|
rescue Torque::MissingProjectError, Torque::InvalidProjectError => e
|
165
180
|
puts "ABORTING"
|
166
181
|
puts e.message
|
167
182
|
puts "Run 'torque project' to set your Pivotal Tracker project"
|
168
|
-
(e.is_a? Torque::MissingProjectError) ? (exit
|
183
|
+
(e.is_a? Torque::MissingProjectError) ? (exit 9) : (exit 10)
|
169
184
|
rescue Torque::PivotalAPIError => e
|
170
185
|
puts "ABORTING"
|
171
186
|
puts e.message
|
172
|
-
exit
|
173
|
-
|
174
|
-
puts "ABORTING"
|
175
|
-
puts e.message
|
176
|
-
puts "Run 'torque config' in this directory, or change your working directory"
|
177
|
-
exit 8
|
178
|
-
rescue Torque::MissingOutputDirectoryError => e
|
179
|
-
puts "ABORTING"
|
180
|
-
puts e.message
|
181
|
-
puts "Run 'torque config -o [directory]' to create/change your output directory"
|
182
|
-
exit 9
|
187
|
+
exit 11
|
188
|
+
|
183
189
|
rescue ArgumentError => e
|
184
190
|
puts "ABORTING"
|
185
191
|
puts e.message
|
186
|
-
exit
|
187
|
-
end
|
192
|
+
exit 12
|
193
|
+
end
|
data/lib/torque.rb
CHANGED
@@ -17,8 +17,6 @@ class Torque
|
|
17
17
|
# @param options A hash of the settings to use for
|
18
18
|
# @param settings An instance of Torque::Settings (default: Torque::Settings.new)
|
19
19
|
# @param settings An instance of Torque::FileSystem (default: Torque::FileSystem.new)
|
20
|
-
#
|
21
|
-
# Creates a new instance of Torque
|
22
20
|
def initialize(options, settings=nil, fs=nil)
|
23
21
|
@fs = fs || FileSystem.new
|
24
22
|
@settings = settings || Settings.new(options, @fs)
|
@@ -29,10 +27,10 @@ class Torque
|
|
29
27
|
##
|
30
28
|
# @param project_html An html string containing all the project's stories
|
31
29
|
# @param project_name The name of the current project
|
32
|
-
# @param
|
30
|
+
# @param iterations_on True if the stories are organized into iterations, else false
|
33
31
|
#
|
34
|
-
#
|
35
|
-
def generate_notes(project_html, project_name,
|
32
|
+
# @return A string comprising the release notes document
|
33
|
+
def generate_notes(project_html, project_name, iterations_on=false)
|
36
34
|
|
37
35
|
notes_string = ""
|
38
36
|
|
@@ -41,12 +39,13 @@ class Torque
|
|
41
39
|
notes_string += "Release Notes\n"
|
42
40
|
notes_string += "Project #{@settings.project} - '#{project_name}'\n"
|
43
41
|
|
44
|
-
if
|
42
|
+
if iterations_on
|
45
43
|
# Past iterations header
|
46
44
|
notes_string += "Last "
|
47
|
-
notes_string += @settings.iterations == 1 \
|
48
|
-
?
|
49
|
-
:
|
45
|
+
notes_string += (@settings.iterations == 1 \
|
46
|
+
? "iteration"
|
47
|
+
: "#{@settings.iterations} iterations"
|
48
|
+
)
|
50
49
|
notes_string += "\n"
|
51
50
|
|
52
51
|
elsif @settings.custom_date_range
|
@@ -71,7 +70,7 @@ class Torque
|
|
71
70
|
html_parser.add_date_filter(@settings.accept_from, @settings.accept_to) unless @settings.iterations
|
72
71
|
html_parser.add_field_filters(@settings.filters) if @settings.filters_on
|
73
72
|
|
74
|
-
if
|
73
|
+
if iterations_on
|
75
74
|
# Adds each iteration
|
76
75
|
|
77
76
|
iteration_list = html_parser.process_project_iterations(project_html)
|
@@ -86,7 +85,10 @@ class Torque
|
|
86
85
|
|
87
86
|
iteration.sort_stories
|
88
87
|
iteration.stories.each do |story|
|
89
|
-
notes_string +=
|
88
|
+
notes_string += @settings.format_string.apply(story)
|
89
|
+
notes_string += "\n"
|
90
|
+
notes_string += "\n"
|
91
|
+
|
90
92
|
print_if_verbose "[Torque] (#{story.date_accepted.strftime(@date_format)}) #{story.name}"
|
91
93
|
end
|
92
94
|
end
|
@@ -104,7 +106,10 @@ class Torque
|
|
104
106
|
|
105
107
|
stories = Story.sort_list(stories)
|
106
108
|
stories.each do |story|
|
107
|
-
notes_string +=
|
109
|
+
notes_string += @settings.format_string.apply(story)
|
110
|
+
notes_string += "\n"
|
111
|
+
notes_string += "\n"
|
112
|
+
|
108
113
|
print_if_verbose "[Torque] (#{story.date_accepted.strftime(@date_format)}) #{story.name}"
|
109
114
|
end
|
110
115
|
|
@@ -158,6 +163,8 @@ class Torque
|
|
158
163
|
end
|
159
164
|
|
160
165
|
##
|
166
|
+
# @return The generated notes
|
167
|
+
#
|
161
168
|
# The method run by Torque on the command line. Generates the notes, writes them to file, optionally emails them
|
162
169
|
def execute
|
163
170
|
|
@@ -219,7 +226,7 @@ class Torque
|
|
219
226
|
|
220
227
|
print_if_not_silent "Notes generated!"
|
221
228
|
|
222
|
-
# Emails the release notes to
|
229
|
+
# Emails the release notes to the mailing list
|
223
230
|
|
224
231
|
email_notes(notes_string, project_name) if(@settings.email)
|
225
232
|
|
@@ -228,27 +235,6 @@ class Torque
|
|
228
235
|
|
229
236
|
private
|
230
237
|
|
231
|
-
# story: A Story object
|
232
|
-
#
|
233
|
-
# Generates a string of release notes for the story
|
234
|
-
# If verbose, prints a statement saying the story was added to stdout
|
235
|
-
def generate_story_string(story)
|
236
|
-
notes_string = ""
|
237
|
-
|
238
|
-
notes_string += "#{story.story_id}\n"
|
239
|
-
notes_string += "#{story.name}\n"
|
240
|
-
notes_string += "Accepted on "+story.date_accepted.strftime(@date_format)+"\n"
|
241
|
-
notes_string += "https://www.pivotaltracker.com/story/show/#{story.story_id}\n"
|
242
|
-
|
243
|
-
descArray = story.description.split("\n")
|
244
|
-
descArray.length.times do |i|
|
245
|
-
notes_string += "\t"+descArray[i]+"\n"
|
246
|
-
end
|
247
|
-
|
248
|
-
notes_string += "\n"
|
249
|
-
notes_string
|
250
|
-
end
|
251
|
-
|
252
238
|
# Prints a message if silent is not on
|
253
239
|
def print_if_not_silent(msg)
|
254
240
|
puts msg unless @settings.silent
|
data/lib/torque/date_settings.rb
CHANGED
@@ -23,14 +23,14 @@ class Torque
|
|
23
23
|
##
|
24
24
|
# Generates the accept_from and accept_to dates if they don't already exist
|
25
25
|
#
|
26
|
-
#
|
26
|
+
# @return [accept_from, accept_to]
|
27
27
|
def get_dates
|
28
28
|
generate_dates if !@accept_from || !@accept_to
|
29
29
|
return @accept_from, @accept_to
|
30
30
|
end
|
31
31
|
|
32
32
|
##
|
33
|
-
#
|
33
|
+
# @return True if the date range was set manually via command line, else false
|
34
34
|
def custom_date_range?
|
35
35
|
generate_dates if !@custom_date_range
|
36
36
|
return @custom_date_range
|
data/lib/torque/field_filter.rb
CHANGED
@@ -14,15 +14,11 @@ class Torque
|
|
14
14
|
|
15
15
|
##
|
16
16
|
# @param field A symbol representing the field to filter
|
17
|
-
# @
|
18
|
-
#
|
19
|
-
#
|
20
|
-
# * :label
|
21
|
-
# * :owner
|
22
|
-
# * :type
|
17
|
+
# @option field [Symbol] :label
|
18
|
+
# @option field [Symbol] :owner
|
19
|
+
# @option field [Symbol] :type
|
23
20
|
#
|
24
|
-
#
|
25
|
-
# "Adam" would match "Adam Barnes" or "John Adam Smith" or "Joe Quincy Adam".
|
21
|
+
# @param contents The contents of the field filter
|
26
22
|
#
|
27
23
|
# The contents should be a list of values speparated by "," or "+", where AND is signified by "+" and OR is signified
|
28
24
|
# by ",", and "+" has a higher precedence than ",". For example,
|
@@ -33,6 +29,9 @@ class Torque
|
|
33
29
|
#
|
34
30
|
# (ios AND android) OR (ios AND web)
|
35
31
|
#
|
32
|
+
# An :owner filter with no spaces in it will filter separately by first/middle/last name. For instance,
|
33
|
+
# "Adam" would match "Adam Barnes" or "John Adam Smith" or "Joe Quincy Adam".
|
34
|
+
#
|
36
35
|
def initialize(field, contents="")
|
37
36
|
@field = field
|
38
37
|
@contents = contents
|
@@ -99,7 +98,7 @@ class Torque
|
|
99
98
|
##
|
100
99
|
# Returns a string representation of the FieldFilter
|
101
100
|
def to_s
|
102
|
-
"#{@field}
|
101
|
+
"#{@field} = #{@contents}"
|
103
102
|
end
|
104
103
|
|
105
104
|
|
data/lib/torque/file_system.rb
CHANGED
@@ -9,15 +9,13 @@ class Torque
|
|
9
9
|
# Supports:
|
10
10
|
#
|
11
11
|
# * File creation, reading, line-by-line iteration, and overwriting
|
12
|
-
#
|
13
12
|
# * Directory creation
|
14
|
-
#
|
15
13
|
# * Pathname checking
|
16
14
|
#
|
17
15
|
class FileSystem
|
18
16
|
|
19
17
|
def initialize
|
20
|
-
# Do nothing
|
18
|
+
# Do nothing; the file system's properties are by definition global
|
21
19
|
end
|
22
20
|
|
23
21
|
##
|
@@ -31,13 +29,13 @@ class Torque
|
|
31
29
|
end
|
32
30
|
|
33
31
|
##
|
34
|
-
#
|
32
|
+
# @return An iterator over each line of a file
|
35
33
|
def file_each_line(filename)
|
36
34
|
File.open(filename, "r").each_line
|
37
35
|
end
|
38
36
|
|
39
37
|
##
|
40
|
-
#
|
38
|
+
# @return The contents of a file
|
41
39
|
def file_read(filename)
|
42
40
|
File.read(filename)
|
43
41
|
end
|
@@ -61,7 +59,9 @@ class Torque
|
|
61
59
|
end
|
62
60
|
|
63
61
|
##
|
64
|
-
#
|
62
|
+
# @param pathname The pathname to test
|
63
|
+
#
|
64
|
+
# @return True if the pathname exists, else false
|
65
65
|
def path_exist?(pathname)
|
66
66
|
Pathname.new(pathname).exist?
|
67
67
|
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
require 'date'
|
2
|
+
|
3
|
+
class Torque
|
4
|
+
|
5
|
+
##
|
6
|
+
# Applies a format string to stories, generating custom string output for each story
|
7
|
+
#
|
8
|
+
# Parameters:
|
9
|
+
# * %a => Date accepted (MM/DD)
|
10
|
+
# * %A => Date accepted (YYYY/MM/DD)
|
11
|
+
# * %d => Description
|
12
|
+
# * %D => Description (tabbed once on each newline)
|
13
|
+
# * %e => Estimate
|
14
|
+
# * %i => ID
|
15
|
+
# * %l => Labels (separated by ", ")
|
16
|
+
# * %n => Newline character
|
17
|
+
# * %N => Name
|
18
|
+
# * %o => Owner of the story
|
19
|
+
# * %p => ID of the story's project
|
20
|
+
# * %u => URL pointing to the story
|
21
|
+
# * %t => Tab character
|
22
|
+
# * %T => Type (feature, bug, etc)
|
23
|
+
class FormatString
|
24
|
+
|
25
|
+
##
|
26
|
+
# @param format_string The format string to use
|
27
|
+
def initialize(format_string)
|
28
|
+
@format_string = format_string
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Returns the deafault format string to use
|
33
|
+
def self.default
|
34
|
+
"%i%n%N%nAccepted on %A%n%u%n%D"
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# @param story A Torque::Story object
|
39
|
+
#
|
40
|
+
# @return A string representing the story formatted according to the format string
|
41
|
+
def apply(story)
|
42
|
+
|
43
|
+
story_string = @format_string.clone
|
44
|
+
|
45
|
+
# %a
|
46
|
+
a = (story.date_accepted ? story.date_accepted.strftime("%m/%d") : "")
|
47
|
+
story_string.gsub!("%a", "#{a}")
|
48
|
+
|
49
|
+
# %A
|
50
|
+
aa = (story.date_accepted ? story.date_accepted.strftime("%Y/%m/%d") : "")
|
51
|
+
story_string.gsub!("%A", "#{aa}")
|
52
|
+
|
53
|
+
# %d
|
54
|
+
d = story.description
|
55
|
+
story_string.gsub!("%d", "#{d}")
|
56
|
+
|
57
|
+
# %D
|
58
|
+
dd = ""
|
59
|
+
story.description.each_line {|line| dd += "\t#{line}"} if story.description
|
60
|
+
story_string.gsub!("%D", "#{dd}")
|
61
|
+
|
62
|
+
# %e
|
63
|
+
e = story.estimate
|
64
|
+
story_string.gsub!("%e", "#{e}")
|
65
|
+
|
66
|
+
# %i
|
67
|
+
i = story.id
|
68
|
+
story_string.gsub!("%i", "#{i}")
|
69
|
+
|
70
|
+
# %l
|
71
|
+
l = (story.labels ? story.labels.join(", ") : "")
|
72
|
+
story_string.gsub!("%l", "#{l}")
|
73
|
+
|
74
|
+
# %n
|
75
|
+
n = "\n"
|
76
|
+
story_string.gsub!("%n", "#{n}")
|
77
|
+
|
78
|
+
# %N
|
79
|
+
nn = story.name
|
80
|
+
story_string.gsub!("%N", "#{nn}")
|
81
|
+
|
82
|
+
# %o
|
83
|
+
o = story.owner
|
84
|
+
story_string.gsub!("%o", "#{o}")
|
85
|
+
|
86
|
+
# %p
|
87
|
+
p = story.project_id
|
88
|
+
story_string.gsub!("%p", "#{p}")
|
89
|
+
|
90
|
+
# %t
|
91
|
+
t = "\t"
|
92
|
+
story_string.gsub!("%t", "#{t}")
|
93
|
+
|
94
|
+
# %T
|
95
|
+
tt = story.type
|
96
|
+
story_string.gsub!("%T", "#{tt}")
|
97
|
+
|
98
|
+
# %u
|
99
|
+
u = story.url
|
100
|
+
story_string.gsub!("%u", "#{u}")
|
101
|
+
|
102
|
+
story_string
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|