torque 0.3.1 → 0.4.0
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.
- 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
|