ditz 0.4 → 0.5

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.
@@ -1,33 +1,28 @@
1
1
  <table>
2
- <% issues.sort_by { |i| i.sort_order }.each do |i| %>
3
- <tr>
4
- <td class="issuestatus_<%= i.status %>">
5
- <% if i.closed? %>
6
- <%= i.disposition_string %>
7
- <% else %>
8
- <%= i.status_string %>
9
- <% end %>
10
- </td>
11
- <td class="issuename">
12
- <%= fancy_issue_link_for i %>
13
- <%= i.bug? ? '(bug)' : '' %>
14
- </td>
15
- <% if show_release %>
16
- <td class="issuerelease">
17
- <% if i.release %>
18
- <% r = project.release_for i.release %>
19
- in <%= link_to r, "release #{i.release}" %>
20
- (<%= r.status %>)
21
- <% else %>
22
- <% end %>
23
- </td>
24
- <% end %>
25
- <% if show_component %>
26
- <td class="issuecomponent">
27
- component <%= link_to project.component_for(i.component), i.component %>
2
+ <tbody>
3
+ <% issues.sort_by { |i| i.creation_time }.reverse.each do |i| %>
4
+ <tr>
5
+ <td> <%= issue_status_img_for i %> </td>
6
+ <td class="littledate"><%= i.creation_time.pretty_date %></td>
7
+ <td class="issuename">
8
+ <%= issue_link_for i %>
28
9
  </td>
29
- <% end %>
30
- </tr>
31
- <% end %>
10
+ <% if show_release %>
11
+ <td class="issuerelease">
12
+ <% if i.release %>
13
+ <% r = project.release_for i.release %>
14
+ <%= link_to r, i.release %>
15
+ <% else %>
16
+ <% end %>
17
+ </td>
18
+ <% end %>
19
+ <% if show_component %>
20
+ <td class="issuecomponent">
21
+ component <%= link_to project.component_for(i.component), i.component %>
22
+ </td>
23
+ <% end %>
24
+ </tr>
25
+ <% end %>
26
+ </tbody>
32
27
  </table>
33
28
 
@@ -85,7 +85,7 @@ module Lowline
85
85
  ans = Readline::readline(prompt)
86
86
  else
87
87
  print prompt
88
- ans = gets.strip
88
+ ans = STDIN.gets.strip
89
89
  end
90
90
  if opts[:default]
91
91
  ans = opts[:default] if ans.blank?
@@ -98,11 +98,10 @@ module Lowline
98
98
 
99
99
  def ask_via_editor q, default=nil
100
100
  fn = run_editor do |f|
101
+ f.puts(default || "")
101
102
  f.puts q.gsub(/^/, "## ")
102
103
  f.puts "##"
103
- f.puts "## Enter your text below. Lines starting with a '#' will be ignored."
104
- f.puts
105
- f.puts default if default
104
+ f.puts "## Enter your text above. Lines starting with a '#' will be ignored."
106
105
  end
107
106
  return unless fn
108
107
  IO.read(fn).gsub(/^#.*$/, "").multistrip
@@ -115,7 +114,7 @@ module Lowline
115
114
  if Ditz::has_readline?
116
115
  line = Readline::readline('> ')
117
116
  else
118
- (line = gets) && line.strip!
117
+ (line = STDIN.gets) && line.strip!
119
118
  end
120
119
  if line
121
120
  if Ditz::has_readline?
@@ -142,7 +141,7 @@ module Lowline
142
141
  def ask_yon q
143
142
  while true
144
143
  print "#{q} (y/n): "
145
- a = gets.strip
144
+ a = STDIN.gets.strip
146
145
  break a if a =~ /^[yn]$/i
147
146
  end =~ /y/i
148
147
  end
@@ -36,17 +36,37 @@ end
36
36
  class Project < ModelObject
37
37
  class Error < StandardError; end
38
38
 
39
- attr_accessor :pathname
40
-
41
- field :name, :default_generator => lambda { File.basename(Dir.pwd) }
39
+ field :name, :prompt => "Project name", :default_generator => lambda { File.basename(Dir.pwd) }
42
40
  field :version, :default => Ditz::VERSION, :ask => false
43
41
  field :components, :multi => true, :generator => :get_components
44
42
  field :releases, :multi => true, :ask => false
45
43
 
44
+ attr_accessor :pathname
45
+
46
46
  ## issues are not model fields proper, so we build up their interface here.
47
- attr_accessor :issues
48
- def add_issue issue; added_issues << issue; issues << issue end
49
- def drop_issue issue; deleted_issues << issue if issues.delete issue end
47
+ attr_reader :issues
48
+ def issues= issues
49
+ @issues = issues
50
+ @issues.each { |i| i.project = self }
51
+ assign_issue_names!
52
+ issues
53
+ end
54
+
55
+ def add_issue issue
56
+ added_issues << issue
57
+ issues << issue
58
+ issue.project = self
59
+ assign_issue_names!
60
+ issue
61
+ end
62
+
63
+ def drop_issue issue
64
+ if issues.delete issue
65
+ deleted_issues << issue
66
+ assign_issue_names!
67
+ end
68
+ end
69
+
50
70
  def added_issues; @added_issues ||= [] end
51
71
  def deleted_issues; @deleted_issues ||= [] end
52
72
 
@@ -61,8 +81,9 @@ EOS
61
81
  ([name] + comp_names).uniq.map { |n| Component.create_interactively :with => { :name => n } }
62
82
  end
63
83
 
64
- def issue_for issue_name
65
- issues.find { |i| i.name == issue_name }
84
+ def issues_for ident
85
+ by_name = issues.find { |i| i.name == ident }
86
+ by_name ? [by_name] : issues.select { |i| i.id =~ /^#{Regexp::escape ident}/ }
66
87
  end
67
88
 
68
89
  def component_for component_name
@@ -106,6 +127,12 @@ EOS
106
127
  raise Error, "more than one release named #{dup.inspect}"
107
128
  end
108
129
  end
130
+
131
+ def self.from *a
132
+ p = super(*a)
133
+ p.validate!
134
+ p
135
+ end
109
136
  end
110
137
 
111
138
  class Issue < ModelObject
@@ -193,6 +220,7 @@ class Issue < ModelObject
193
220
  def feature?; type == :feature end
194
221
  def unassigned?; release.nil? end
195
222
  def assigned?; !unassigned? end
223
+ def paused?; status == :paused end
196
224
 
197
225
  def start_work who, comment; change_status :in_progress, who, comment end
198
226
  def stop_work who, comment
@@ -215,27 +243,29 @@ class Issue < ModelObject
215
243
  end
216
244
  private :change_status
217
245
 
218
- def change hash, who, comment
246
+ def change hash, who, comment, silent
219
247
  what = []
220
248
  if title != hash[:title]
221
- what << "changed title"
249
+ what << "title"
222
250
  self.title = hash[:title]
223
251
  end
224
252
 
225
253
  if desc != hash[:description]
226
- what << "changed description"
254
+ what << "description"
227
255
  self.desc = hash[:description]
228
256
  end
229
257
 
230
258
  if reporter != hash[:reporter]
231
- what << "changed reporter"
259
+ what << "reporter"
232
260
  self.reporter = hash[:reporter]
233
261
  end
234
262
 
235
- unless what.empty?
236
- log what.join(", "), who, comment
263
+ unless what.empty? || silent
264
+ log "edited " + what.join(", "), who, comment
237
265
  true
238
266
  end
267
+
268
+ !what.empty?
239
269
  end
240
270
 
241
271
  def assign_to_release release, who, comment
@@ -288,15 +318,18 @@ end
288
318
  class Config < ModelObject
289
319
  field :name, :prompt => "Your name", :default_generator => :get_default_name
290
320
  field :email, :prompt => "Your email address", :default_generator => :get_default_email
291
- field :issue_dir, :ask => false, :default => "bugs"
321
+ field :issue_dir, :prompt => "Directory to store issues state in", :default => "bugs"
292
322
 
293
323
  def user; "#{name} <#{email}>" end
294
324
 
295
325
  def get_default_name
296
326
  require 'etc'
297
327
 
298
- name = Etc.getpwnam(ENV["USER"])
299
- name = name ? name.gecos.split(/,/).first : ""
328
+ name = if ENV["USER"]
329
+ pwent = Etc.getpwnam ENV["USER"]
330
+ pwent ? pwent.gecos.split(/,/).first : nil
331
+ end
332
+ name || "Ditz User"
300
333
  end
301
334
 
302
335
  def get_default_email
@@ -90,6 +90,10 @@ class ModelObject
90
90
  @serialized_values[name] = o
91
91
  end
92
92
 
93
+ define_method "__serialized_#{name}" do
94
+ @serialized_values[name]
95
+ end
96
+
93
97
  define_method name do
94
98
  return @values[name] if @values.member?(name)
95
99
  @values[name] = deserialized_form_of name, @serialized_values[name]
@@ -110,6 +114,14 @@ class ModelObject
110
114
  returning YAML::load_file(fn) do |o|
111
115
  raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
112
116
  o.pathname = fn if o.respond_to? :pathname=
117
+
118
+ o.class.fields.each do |f, opts|
119
+ m = "__serialized_#{f}"
120
+ if opts[:multi] && o.send(m).nil?
121
+ $stderr.puts "Warning: corrected nil multi-field #{f}"
122
+ o.send "#{m}=", []
123
+ end
124
+ end
113
125
  end
114
126
  end
115
127
 
@@ -170,37 +182,83 @@ class ModelObject
170
182
  def changed!; @changed = true end
171
183
  def unchanged!; @changed = false end
172
184
 
173
- def self.create_interactively opts={}
174
- o = self.new
175
- args = opts[:args] || []
176
- @fields.each do |name, field_opts|
177
- val = if opts[:with] && opts[:with][name]
178
- opts[:with][name]
179
- elsif field_opts[:generator].is_a? Proc
180
- field_opts[:generator].call(*args)
181
- elsif field_opts[:generator]
182
- o.send field_opts[:generator], *args
183
- elsif field_opts[:ask] == false # nil counts as true here
184
- field_opts[:default] || (field_opts[:multi] ? [] : nil)
185
- else
186
- q = field_opts[:prompt] || name.to_s.capitalize
187
- if field_opts[:multiline]
188
- ask_multiline q
185
+ class << self
186
+ ## creates the object, prompting the user when necessary. can take
187
+ ## a :with => { hash } parameter for pre-filling model fields.
188
+ ##
189
+ ## can also take a :defaults_from => obj parameter for pre-filling model
190
+ ## fields from another object with (some of) those fields. kinda like a
191
+ ## bizarre interactive copy constructor.
192
+ def create_interactively opts={}
193
+ o = self.new
194
+ generator_args = opts[:args] || []
195
+ @fields.each do |name, field_opts|
196
+ val = if opts[:with] && opts[:with][name]
197
+ opts[:with][name]
198
+ elsif(found, x = generate_field_value(o, field_opts, generator_args)) && found
199
+ x
189
200
  else
190
- default = if field_opts[:default_generator].is_a? Proc
191
- field_opts[:default_generator].call(*args)
192
- elsif field_opts[:default_generator]
193
- o.send field_opts[:default_generator], *args
194
- elsif field_opts[:default]
195
- field_opts[:default]
201
+ q = field_opts[:prompt] || name.to_s.capitalize
202
+ if field_opts[:multiline]
203
+ ## multiline options currently aren't allowed to have a default
204
+ ## value, so just ask.
205
+ ask_multiline q
206
+ else
207
+ default = if opts[:defaults_from] && opts[:defaults_from].respond_to?(name) && (x = opts[:defaults_from].send(name))
208
+ x
209
+ else
210
+ default = generate_field_default o, field_opts, generator_args
211
+ end
212
+ ask q, :default => default
196
213
  end
197
-
198
- ask q, :default => default
199
214
  end
215
+ o.send "#{name}=", val
216
+ end
217
+ o
218
+ end
219
+
220
+ ## creates the object, filling in fields from 'vals', and throwing a
221
+ ## ModelError when it can't find all the requisite fields
222
+ def create generator_args, vals={}
223
+ o = self.new
224
+ @fields.each do |name, opts|
225
+ val = if vals[name]
226
+ vals[name]
227
+ elsif(found, x = generate_field_value(o, opts, generator_args)) && found
228
+ x
229
+ else
230
+ raise ModelError, "missing required field #{name}"
231
+ end
232
+ o.send "#{name}=", val
233
+ end
234
+ o
235
+ end
236
+
237
+ private
238
+
239
+ ## get the value for a field if it can be automatically determined
240
+ ## returns [success, value] (because a successful value can be ni)
241
+ def generate_field_value o, opts, args
242
+ if opts[:generator].is_a? Proc
243
+ [true, opts[:generator].call(*args)]
244
+ elsif opts[:generator]
245
+ [true, o.send(opts[:generator], *args)]
246
+ elsif opts[:ask] == false # nil counts as true here
247
+ [true, opts[:default] || (opts[:multi] ? [] : nil)]
248
+ else
249
+ [false, nil]
250
+ end
251
+ end
252
+
253
+ def generate_field_default o, opts, args
254
+ if opts[:default_generator].is_a? Proc
255
+ opts[:default_generator].call(*args)
256
+ elsif opts[:default_generator]
257
+ o.send opts[:default_generator], *args
258
+ elsif opts[:default]
259
+ opts[:default]
200
260
  end
201
- o.send("#{name}=", val)
202
261
  end
203
- o
204
262
  end
205
263
  end
206
264
 
@@ -9,9 +9,10 @@ class Operator
9
9
  def method_to_op meth; meth.to_s.gsub("_", "-") end
10
10
  def op_to_method op; op.gsub("-", "_").intern end
11
11
 
12
- def operation method, desc, *args_spec
12
+ def operation method, desc, *args_spec, &options_blk
13
13
  @operations ||= {}
14
- @operations[method] = { :desc => desc, :args_spec => args_spec }
14
+ @operations[method] = { :desc => desc, :args_spec => args_spec,
15
+ :options_blk => options_blk }
15
16
  end
16
17
 
17
18
  def operations
@@ -19,6 +20,11 @@ class Operator
19
20
  end
20
21
  def has_operation? op; @operations.member? op_to_method(op) end
21
22
 
23
+ def build_opts method, args
24
+ options_blk = @operations[method][:options_blk]
25
+ options_blk and options args, &options_blk or nil
26
+ end
27
+
22
28
  ## parse the specs, and the commandline arguments, and resolve them. does
23
29
  ## typechecking but currently doesn't check for open_issues actually being
24
30
  ## open, unstarted_issues being unstarted, etc. probably will check for
@@ -50,7 +56,13 @@ class Operator
50
56
  when :issue, :open_issue, :unstarted_issue, :started_issue, :assigned_issue
51
57
  ## issue completion sticks the title on there, so this will strip it off
52
58
  valr = val.sub(/\A(\w+-\d+)_.*$/,'\1')
53
- project.issue_for(valr) or raise Error, "no issue with name #{val}"
59
+ issues = project.issues_for valr
60
+ case issues.size
61
+ when 0; raise Error, "no issue with name #{val.inspect}"
62
+ when 1; issues.first
63
+ else
64
+ raise Error, "multiple issues matching name #{val.inspect}"
65
+ end
54
66
  when :release, :unreleased_release
55
67
  if val == "unassigned"
56
68
  :unassigned
@@ -96,7 +108,13 @@ class Operator
96
108
 
97
109
  def do op, project, config, args
98
110
  meth = self.class.op_to_method(op)
111
+
112
+ # Parse options, removing them from args
113
+ opts = self.class.build_opts meth, args
99
114
  built_args = self.class.build_args project, meth, args
115
+
116
+ built_args.unshift opts if opts
117
+
100
118
  send meth, project, config, *built_args
101
119
  end
102
120
 
@@ -109,8 +127,15 @@ class Operator
109
127
  Project.create_interactively
110
128
  end
111
129
 
112
- operation :help, "List all registered commands", :maybe_command
113
- def help project, config, command
130
+ operation :help, "List all registered commands", :maybe_command do
131
+ opt :cow, "Activate super cow powers", :default => false
132
+ end
133
+ def help project, config, opts, command
134
+ if opts[:cow]
135
+ puts "MOO!"
136
+ puts "All is well with the world now. A bit more methane though."
137
+ return
138
+ end
114
139
  return help_single(command) if command
115
140
  puts <<EOS
116
141
  Ditz commands:
@@ -145,11 +170,32 @@ Usage: ditz #{name} #{args}
145
170
  EOS
146
171
  end
147
172
 
148
- operation :add, "Add an issue"
149
- def add project, config
173
+ ## gets a comment from the user, assuming the standard argument setup
174
+ def get_comment opts
175
+ comment = if opts[:no_comment]
176
+ nil
177
+ elsif opts[:comment]
178
+ opts[:comment]
179
+ else
180
+ ask_multiline "Comments"
181
+ end
182
+ end
183
+ private :get_comment
184
+
185
+ operation :reconfigure, "Rerun configuration script"
186
+ def reconfigure project, config
187
+ new_config = Config.create_interactively :defaults_from => config
188
+ new_config.save! $opts[:config_file]
189
+ puts "Configuration written."
190
+ end
191
+
192
+ operation :add, "Add an issue" do
193
+ opt :comment, "Specify a comment", :short => 'm', :type => String
194
+ opt :no_comment, "Skip asking for a comment", :default => false
195
+ end
196
+ def add project, config, opts
150
197
  issue = Issue.create_interactively(:args => [config, project]) or return
151
- comment = ask_multiline "Comments" unless $opts[:no_comment]
152
- issue.log "created", config.user, comment
198
+ issue.log "created", config.user, get_comment(opts)
153
199
  project.add_issue issue
154
200
  project.assign_issue_names!
155
201
  puts "Added issue #{issue.name}."
@@ -161,12 +207,14 @@ EOS
161
207
  puts "Dropped #{issue.name}. Note that other issue names may have changed."
162
208
  end
163
209
 
164
- operation :add_release, "Add a release", :maybe_name
165
- def add_release project, config, maybe_name
210
+ operation :add_release, "Add a release", :maybe_name do
211
+ opt :comment, "Specify a comment", :short => 'm', :type => String
212
+ opt :no_comment, "Skip asking for a comment", :default => false
213
+ end
214
+ def add_release project, config, opts, maybe_name
166
215
  puts "Adding release #{maybe_name}." if maybe_name
167
216
  release = Release.create_interactively(:args => [project, config], :with => { :name => maybe_name }) or return
168
- comment = ask_multiline "Comments" unless $opts[:no_comment]
169
- release.log "created", config.user, comment
217
+ release.log "created", config.user, get_comment(opts)
170
218
  project.add_release release
171
219
  puts "Added release #{release.name}."
172
220
  end
@@ -178,13 +226,15 @@ EOS
178
226
  puts "Added component #{component.name}."
179
227
  end
180
228
 
181
- operation :add_reference, "Add a reference to an issue", :issue
182
- def add_reference project, config, issue
229
+ operation :add_reference, "Add a reference to an issue", :issue do
230
+ opt :comment, "Specify a comment", :short => 'm', :type => String
231
+ opt :no_comment, "Skip asking for a comment", :default => false
232
+ end
233
+ def add_reference project, config, opts, issue
183
234
  puts "Adding a reference to #{issue.name}: #{issue.title}."
184
235
  reference = ask "Reference"
185
- comment = ask_multiline "Comments" unless $opts[:no_comment]
186
236
  issue.add_reference reference
187
- issue.log "added reference #{issue.references.size}", config.user, comment
237
+ issue.log "added reference #{issue.references.size}", config.user, get_comment(opts)
188
238
  puts "Added reference to #{issue.name}."
189
239
  end
190
240
 
@@ -208,7 +258,9 @@ EOS
208
258
  "%2d/%2d %s" % [nc, num, type.to_s.pluralize(num, false)]
209
259
  end
210
260
 
211
- bar = if r != :unassigned && r.released?
261
+ bar = if r == :unassigned
262
+ ""
263
+ elsif r.released?
212
264
  "(released)"
213
265
  elsif issues.empty?
214
266
  "(no issues)"
@@ -248,33 +300,44 @@ EOS
248
300
  join
249
301
  end
250
302
 
251
- def todo_list_for issues
303
+ def todo_list_for issues, opts={}
252
304
  return if issues.empty?
253
305
  name_len = issues.max_of { |i| i.name.length }
254
306
  issues.map do |i|
255
- sprintf "%s %#{name_len}s: %s\n", i.status_widget, i.name, i.title
307
+ s = sprintf "%s %#{name_len}s: %s", i.status_widget, i.name, i.title
308
+ s += " [#{i.release}]" if opts[:show_release] && i.release
309
+ s + "\n"
256
310
  end.join
257
311
  end
258
312
 
259
- operation :todo, "Generate todo list", :maybe_release
260
- def todo project, config, releases
261
- actually_do_todo project, config, releases, false
313
+ def print_todo_list_by_release_for project, issues
314
+ by_release = issues.inject({}) do |h, i|
315
+ r = project.release_for(i.release) || :unassigned
316
+ h[r] ||= []
317
+ h[r] << i
318
+ h
319
+ end
320
+
321
+ (project.releases + [:unassigned]).each do |r|
322
+ next unless by_release.member? r
323
+ puts r == :unassigned ? "Unassigned:" : "#{r.name} (#{r.status}):"
324
+ print todo_list_for(by_release[r])
325
+ puts
326
+ end
262
327
  end
263
328
 
264
- operation :todo_full, "Generate full todo list, including completed items", :maybe_release
265
- def todo_full project, config, releases
266
- actually_do_todo project, config, releases, true
329
+ operation :todo, "Generate todo list", :maybe_release do
330
+ opt :all, "Show all issues, included completed ones", :default => false
331
+ end
332
+ def todo project, config, opts, releases
333
+ actually_do_todo project, config, releases, opts[:all]
267
334
  end
268
335
 
269
336
  def actually_do_todo project, config, releases, full
270
337
  releases ||= project.unreleased_releases + [:unassigned]
271
338
  releases = [*releases]
272
339
  releases.each do |r|
273
- if r == :unassigned
274
- puts "Unassigned:"
275
- else
276
- puts "Version #{r.name} (#{r.status}):"
277
- end
340
+ puts r == :unassigned ? "Unassigned:" : "#{r.name} (#{r.status}):"
278
341
  issues = project.issues_for_release r
279
342
  issues = issues.select { |i| i.open? } unless full
280
343
  puts(todo_list_for(issues.sort_by { |i| i.sort_order }) || "No open issues.")
@@ -287,33 +350,42 @@ EOS
287
350
  ScreenView.new(project, config).render_issue issue
288
351
  end
289
352
 
290
- operation :start, "Start work on an issue", :unstarted_issue
291
- def start project, config, issue
353
+ operation :start, "Start work on an issue", :unstarted_issue do
354
+ opt :comment, "Specify a comment", :short => 'm', :type => String
355
+ opt :no_comment, "Skip asking for a comment", :default => false
356
+ end
357
+ def start project, config, opts, issue
292
358
  puts "Starting work on issue #{issue.name}: #{issue.title}."
293
- comment = ask_multiline "Comments" unless $opts[:no_comment]
294
- issue.start_work config.user, comment
359
+ issue.start_work config.user, get_comment(opts)
295
360
  puts "Recorded start of work for #{issue.name}."
296
361
  end
297
362
 
298
- operation :stop, "Stop work on an issue", :started_issue
299
- def stop project, config, issue
363
+ operation :stop, "Stop work on an issue", :started_issue do
364
+ opt :comment, "Specify a comment", :short => 'm', :type => String
365
+ opt :no_comment, "Skip asking for a comment", :default => false
366
+ end
367
+ def stop project, config, opts, issue
300
368
  puts "Stopping work on issue #{issue.name}: #{issue.title}."
301
- comment = ask_multiline "Comments" unless $opts[:no_comment]
302
- issue.stop_work config.user, comment
369
+ issue.stop_work config.user, get_comment(opts)
303
370
  puts "Recorded work stop for #{issue.name}."
304
371
  end
305
372
 
306
- operation :close, "Close an issue", :open_issue
307
- def close project, config, issue
373
+ operation :close, "Close an issue", :open_issue do
374
+ opt :comment, "Specify a comment", :short => 'm', :type => String
375
+ opt :no_comment, "Skip asking for a comment", :default => false
376
+ end
377
+ def close project, config, opts, issue
308
378
  puts "Closing issue #{issue.name}: #{issue.title}."
309
379
  disp = ask_for_selection Issue::DISPOSITIONS, "disposition", lambda { |x| Issue::DISPOSITION_STRINGS[x] || x.to_s }
310
- comment = ask_multiline "Comments" unless $opts[:no_comment]
311
- issue.close disp, config.user, comment
380
+ issue.close disp, config.user, get_comment(opts)
312
381
  puts "Closed issue #{issue.name} with disposition #{issue.disposition_string}."
313
382
  end
314
383
 
315
- operation :assign, "Assign an issue to a release", :issue, :maybe_release
316
- def assign project, config, issue, maybe_release
384
+ operation :assign, "Assign an issue to a release", :issue, :maybe_release do
385
+ opt :comment, "Specify a comment", :short => 'm', :type => String
386
+ opt :no_comment, "Skip asking for a comment", :default => false
387
+ end
388
+ def assign project, config, opts, issue, maybe_release
317
389
  if maybe_release && maybe_release.name == issue.release
318
390
  raise Error, "issue #{issue.name} already assigned to release #{issue.release}"
319
391
  end
@@ -324,8 +396,6 @@ EOS
324
396
  "not assigned to any release."
325
397
  end
326
398
 
327
- puts "Assigning to release #{maybe_release.name}." if maybe_release
328
-
329
399
  release = maybe_release || begin
330
400
  releases = project.releases.sort_by { |r| (r.release_time || 0).to_i }
331
401
  releases -= [releases.find { |r| r.name == issue.release }] if issue.release
@@ -337,13 +407,15 @@ EOS
337
407
  end
338
408
  end
339
409
  end
340
- comment = ask_multiline "Comments" unless $opts[:no_comment]
341
- issue.assign_to_release release, config.user, comment
410
+ issue.assign_to_release release, config.user, get_comment(opts)
342
411
  puts "Assigned #{issue.name} to #{release.name}."
343
412
  end
344
413
 
345
- operation :set_component, "Set an issue's component", :issue, :maybe_component
346
- def set_component project, config, issue, maybe_component
414
+ operation :set_component, "Set an issue's component", :issue, :maybe_component do
415
+ opt :comment, "Specify a comment", :short => 'm', :type => String
416
+ opt :no_comment, "Skip asking for a comment", :default => false
417
+ end
418
+ def set_component project, config, opts, issue, maybe_component
347
419
  puts "Changing the component of issue #{issue.name}: #{issue.title}."
348
420
 
349
421
  if project.components.size == 1
@@ -359,8 +431,7 @@ EOS
359
431
  components -= [components.find { |r| r.name == issue.component }] if issue.component
360
432
  ask_for_selection(components, "component") { |r| r.name }
361
433
  end
362
- comment = ask_multiline "Comments" unless $opts[:no_comment]
363
- issue.assign_to_component component, config.user, comment
434
+ issue.assign_to_component component, config.user, get_comment(opts)
364
435
  oldname = issue.name
365
436
  project.assign_issue_names!
366
437
  puts <<EOS
@@ -369,18 +440,23 @@ have changed as well.
369
440
  EOS
370
441
  end
371
442
 
372
- operation :unassign, "Unassign an issue from any releases", :assigned_issue
373
- def unassign project, config, issue
443
+ operation :unassign, "Unassign an issue from any releases", :assigned_issue do
444
+ opt :comment, "Specify a comment", :short => 'm', :type => String
445
+ opt :no_comment, "Skip asking for a comment", :default => false
446
+ end
447
+ def unassign project, config, opts, issue
374
448
  puts "Unassigning issue #{issue.name}: #{issue.title}."
375
- comment = ask_multiline "Comments" unless $opts[:no_comment]
376
- issue.unassign config.user, comment
449
+ issue.unassign config.user, get_comment(opts)
377
450
  puts "Unassigned #{issue.name}."
378
451
  end
379
452
 
380
- operation :comment, "Comment on an issue", :issue
381
- def comment project, config, issue
453
+ operation :comment, "Comment on an issue", :issue do
454
+ opt :comment, "Specify a comment", :short => 'm', :type => String
455
+ opt :no_comment, "Skip asking for a comment", :default => false
456
+ end
457
+ def comment project, config, opts, issue
382
458
  puts "Commenting on issue #{issue.name}: #{issue.title}."
383
- comment = ask_multiline "Comments"
459
+ comment = get_comment opts
384
460
  if comment.blank?
385
461
  puts "Empty comment, aborted."
386
462
  else
@@ -398,10 +474,12 @@ EOS
398
474
  end
399
475
  end
400
476
 
401
- operation :release, "Release a release", :unreleased_release
402
- def release project, config, release
403
- comment = ask_multiline "Comments" unless $opts[:no_comment]
404
- release.release! project, config.user, comment
477
+ operation :release, "Release a release", :unreleased_release do
478
+ opt :comment, "Specify a comment", :short => 'm', :type => String
479
+ opt :no_comment, "Skip asking for a comment", :default => false
480
+ end
481
+ def release project, config, opts, release
482
+ release.release! project, config.user, get_comment(opts)
405
483
  puts "Release #{release.name} released!"
406
484
  end
407
485
 
@@ -484,8 +562,12 @@ EOS
484
562
  puts "Archived to #{dir}."
485
563
  end
486
564
 
487
- operation :edit, "Edit an issue", :issue
488
- def edit project, config, issue
565
+ operation :edit, "Edit an issue", :issue do
566
+ opt :comment, "Specify a comment", :short => 'm', :type => String
567
+ opt :no_comment, "Skip asking for a comment", :default => false
568
+ opt :silent, "Don't add a log message detailing the change", :default => false
569
+ end
570
+ def edit project, config, opts, issue
489
571
  data = { :title => issue.title, :description => issue.desc,
490
572
  :reporter => issue.reporter }
491
573
 
@@ -496,12 +578,11 @@ EOS
496
578
  return
497
579
  end
498
580
 
499
- comment = ask_multiline "Comments" unless $opts[:no_comment]
500
-
501
581
  begin
502
582
  edits = YAML.load_file fn
503
- if issue.change edits, config.user, comment
504
- puts "Changed recorded."
583
+ comment = opts[:silent] ? nil : get_comment(opts)
584
+ if issue.change edits, config.user, comment, opts[:silent]
585
+ puts "Change recorded."
505
586
  else
506
587
  puts "No changes."
507
588
  end