ditz 0.4 → 0.5

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