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.
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 1
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/iterations. "
140
+ err_str += "-i/--iterations. "
135
141
  err_str += "Cannot use a custom date range with 'iterations' option"
136
142
  puts err_str
137
- # puts option_parser.help
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 2
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 3) : (exit 4)
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 5) : (exit 6)
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 7
173
- rescue Torque::MissingTorqueInfoFileError => e
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 10
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 iterations True if the stories are organized into iterations, else false
30
+ # @param iterations_on True if the stories are organized into iterations, else false
33
31
  #
34
- # Returns a string comprising the release notes document
35
- def generate_notes(project_html, project_name, iterations=false)
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 @settings.iterations
42
+ if iterations_on
45
43
  # Past iterations header
46
44
  notes_string += "Last "
47
- notes_string += @settings.iterations == 1 \
48
- ? notes_string += "iteration"
49
- : notes_string += "#{@settings.iterations} iterations"
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 iterations
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 += generate_story_string(story)
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 += generate_story_string(story)
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 all specified addresses
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
@@ -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
- # Returns [accept_from, accept_to]
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
- # Returns true if the date range was set manually via command line, else false
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
@@ -14,15 +14,11 @@ class Torque
14
14
 
15
15
  ##
16
16
  # @param field A symbol representing the field to filter
17
- # @param contents The contents of the field filter
18
- #
19
- # Current options for the field are
20
- # * :label
21
- # * :owner
22
- # * :type
17
+ # @option field [Symbol] :label
18
+ # @option field [Symbol] :owner
19
+ # @option field [Symbol] :type
23
20
  #
24
- # An :owner filter with no spaces in it will filter separately by first/middle/last name. For instance,
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}: #{@contents}"
101
+ "#{@field} = #{@contents}"
103
102
  end
104
103
 
105
104
 
@@ -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. The file system's properties are automatically global
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
- # Returns an iterator over each line of a file
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
- # Returns the contents of a file
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
- # Returns true if a pathname exists, else false
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