torque 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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