ursm-ditz 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/Changelog +15 -0
  2. data/INSTALL +20 -0
  3. data/LICENSE +674 -0
  4. data/Manifest.txt +40 -0
  5. data/PLUGINS.txt +140 -0
  6. data/README.txt +43 -27
  7. data/Rakefile +36 -3
  8. data/ReleaseNotes +6 -0
  9. data/bin/ditz +49 -63
  10. data/contrib/completion/ditz.bash +25 -9
  11. data/lib/ditz.rb +52 -7
  12. data/lib/ditz/file-storage.rb +54 -0
  13. data/lib/{hook.rb → ditz/hook.rb} +1 -1
  14. data/lib/{html.rb → ditz/html.rb} +42 -4
  15. data/lib/{lowline.rb → ditz/lowline.rb} +31 -11
  16. data/lib/{model-objects.rb → ditz/model-objects.rb} +53 -21
  17. data/lib/ditz/model.rb +321 -0
  18. data/lib/{operator.rb → ditz/operator.rb} +122 -67
  19. data/lib/ditz/plugins/git-sync.rb +83 -0
  20. data/lib/{plugins → ditz/plugins}/git.rb +57 -18
  21. data/lib/ditz/plugins/issue-claiming.rb +174 -0
  22. data/lib/ditz/plugins/issue-labeling.rb +161 -0
  23. data/lib/{util.rb → ditz/util.rb} +4 -0
  24. data/lib/{view.rb → ditz/view.rb} +0 -0
  25. data/lib/{views.rb → ditz/views.rb} +7 -4
  26. data/man/{ditz.1 → man1/ditz.1} +1 -1
  27. data/setup.rb +1585 -0
  28. data/share/ditz/blue-check.png +0 -0
  29. data/{lib → share/ditz}/component.rhtml +7 -5
  30. data/share/ditz/green-bar.png +0 -0
  31. data/share/ditz/green-check.png +0 -0
  32. data/share/ditz/index.rhtml +130 -0
  33. data/share/ditz/issue.rhtml +119 -0
  34. data/share/ditz/issue_table.rhtml +28 -0
  35. data/share/ditz/red-check.png +0 -0
  36. data/share/ditz/release.rhtml +98 -0
  37. data/share/ditz/style.css +226 -0
  38. data/share/ditz/unassigned.rhtml +23 -0
  39. data/share/ditz/yellow-bar.png +0 -0
  40. metadata +50 -28
  41. data/lib/index.rhtml +0 -113
  42. data/lib/issue.rhtml +0 -111
  43. data/lib/issue_table.rhtml +0 -33
  44. data/lib/model.rb +0 -208
  45. data/lib/plugins/issue-claiming.rb +0 -92
  46. data/lib/release.rhtml +0 -69
  47. data/lib/style.css +0 -127
  48. data/lib/trollop.rb +0 -518
  49. data/lib/unassigned.rhtml +0 -31
  50. data/lib/vendor/yaml_waml.rb +0 -28
@@ -0,0 +1,321 @@
1
+ require 'yaml'
2
+ require "yaml_waml"
3
+ require 'sha1'
4
+ require "ditz/lowline"; include Lowline
5
+ require "ditz/util"
6
+
7
+ class Time
8
+ alias :old_to_yaml :to_yaml
9
+ def to_yaml(opts = {})
10
+ self.utc.old_to_yaml(opts)
11
+ end
12
+ end
13
+
14
+ module Ditz
15
+
16
+ class ModelObject
17
+ class ModelError < StandardError; end
18
+
19
+ def initialize
20
+ @values = {}
21
+ @serialized_values = {}
22
+ self.class.fields.map { |f, opts| @values[f] = [] if opts[:multi] }
23
+ end
24
+
25
+ ## yamlability
26
+ def self.yaml_domain; "ditz.rubyforge.org,2008-03-06" end
27
+ def self.yaml_other_thing; name.split('::').last.dcfirst end
28
+ def to_yaml_type; "!#{self.class.yaml_domain}/#{self.class.yaml_other_thing}" end
29
+ def self.inherited subclass
30
+ YAML.add_domain_type(yaml_domain, subclass.yaml_other_thing) do |type, val|
31
+ o = subclass.new
32
+ val.each do |k, v|
33
+ m = "__serialized_#{k}="
34
+ if o.respond_to? m
35
+ o.send m, v
36
+ else
37
+ $stderr.puts "warning: unknown field #{k.inspect} in YAML for #{type}; ignoring"
38
+ end
39
+ end
40
+ o.class.fields.each do |f, opts|
41
+ m = "__serialized_#{f}"
42
+ if opts[:multi] && o.send(m).nil?
43
+ o.send(m + '=', [])
44
+ end
45
+ end
46
+ o.unchanged!
47
+ o
48
+ end
49
+ end
50
+
51
+ ## override these two to model per-field transformations between disk and
52
+ ## memory.
53
+ ##
54
+ ## convert disk form => memory form
55
+ def deserialized_form_of field, value
56
+ value
57
+ end
58
+
59
+ ## convert memory form => disk form
60
+ def serialized_form_of field, value
61
+ value
62
+ end
63
+
64
+ ## Add a field to a model object
65
+ ##
66
+ ## The options you specify here determine how the field is populated when an
67
+ ## instance of this object is created. Objects can be created interactively,
68
+ ## with #create_interactively, or non-interactively, with #create, and the
69
+ ## creation mode, combined with these options, determine how the field is
70
+ ## populated on a new model object.
71
+ ##
72
+ ## The default behavior is to simply prompt the user with the field name when
73
+ ## in interactive mode, and to raise an exception if the value is not passed
74
+ ## to #create in non-interactive mode.
75
+ ##
76
+ ## Options:
77
+ ## :interactive_generator => a method name or Proc that will be called to
78
+ ## return the value of this field, if the model object is created
79
+ ## interactively.
80
+ ## :generator => a method name or Proc that will be called to return the
81
+ ## value of this field. If the model object is created interactively, and
82
+ ## a :interactive_generator option is specified, that will be used instead.
83
+ ## :multi => a boolean determining whether the field has multiple values,
84
+ ## i.e., is an array. If created with :ask => false, will be initialized
85
+ ## to [] instead of to nil. Additionally, the model object will have
86
+ ## #add_<field> and #drop_<field> methods.
87
+ ## :ask => a boolean determining whether, if the model object is created
88
+ ## interactively, the user will be prompted for the value of this field.
89
+ ## TRUE BY DEFAULT. If :interactive_generator or :generator are specified,
90
+ ## those will be called instead.
91
+ ##
92
+ ## If this is true, non-interactive creation
93
+ ## will raise an exception unless the field value is passed as an argument.
94
+ ## If this is false, non-interactive creation will initialize this to nil
95
+ ## (or [] if this field is additionally marked :multi) unless the value is
96
+ ## passed as an argument.
97
+ ## :prompt => a string to display to the user when prompting for the field
98
+ ## value during interactive creation. Not used if :generator or
99
+ ## :interactive_generator is specified.
100
+ ## :multiline => a boolean determining whether to prompt the user for a
101
+ ## multiline answer during interactive creation. Default false. Not used
102
+ ## if :generator or :interactive_generator is specified.
103
+ ## :default => a default value when prompting for the field value during
104
+ ## interactive creation. Not used if :generator, :interactive_generator,
105
+ ## :multiline, or :default_generator is specified.
106
+ ## :default_generator => a method name or Proc which will be called to
107
+ ## generate the default value when prompting for the field value during
108
+ ## interactive creation. Not used if :generator, :interactive_generator,
109
+ ## or :multiline is specified.
110
+ ## :nil_ok => a boolean determining whether, if created in non-interactive
111
+ ## mode and the value for this field is not passed in, (or is passed in
112
+ ## as nil), that's ok. Default is false. This is not necessary if :ask =>
113
+ ## false is specified; it's only necessary for fields that you want an
114
+ ## interactive prompt for, but a nil value is fine.
115
+ def self.field name, opts={}
116
+ @fields ||= [] # can't use a hash because we need to preserve field order
117
+ raise ModelError, "field with name #{name} already defined" if @fields.any? { |k, v| k == name }
118
+ @fields << [name, opts]
119
+
120
+ if opts[:multi]
121
+ single_name = name.to_s.sub(/s$/, "") # oh yeah
122
+ define_method "add_#{single_name}" do |obj|
123
+ array = send(name)
124
+ raise ModelError, "already has a #{single_name} with name #{obj.name.inspect}" if obj.respond_to?(:name) && array.any? { |o| o.name == obj.name }
125
+ changed!
126
+ @serialized_values.delete name
127
+ array << obj
128
+ end
129
+
130
+ define_method "drop_#{single_name}" do |obj|
131
+ return unless send(name).delete obj
132
+ @serialized_values.delete name
133
+ changed!
134
+ obj
135
+ end
136
+ end
137
+
138
+ define_method "#{name}=" do |o|
139
+ changed!
140
+ @serialized_values.delete name
141
+ @values[name] = o
142
+ end
143
+
144
+ define_method "__serialized_#{name}=" do |o|
145
+ changed!
146
+ @values.delete name
147
+ @serialized_values[name] = o
148
+ end
149
+
150
+ define_method "__serialized_#{name}" do
151
+ @serialized_values[name]
152
+ end
153
+
154
+ define_method name do
155
+ return @values[name] if @values.member?(name)
156
+ @values[name] = deserialized_form_of name, @serialized_values[name]
157
+ end
158
+ end
159
+
160
+ def self.field_names; @fields.map { |name, opts| name } end
161
+ class << self
162
+ attr_reader :fields, :values, :serialized_values
163
+ end
164
+
165
+ def self.changes_are_logged
166
+ define_method(:changes_are_logged?) { true }
167
+ field :log_events, :multi => true, :ask => false
168
+ end
169
+
170
+ def self.from fn
171
+ returning YAML::load_file(fn) do |o|
172
+ raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
173
+ o.pathname = fn if o.respond_to? :pathname=
174
+ end
175
+ end
176
+
177
+ def to_s
178
+ "<#{self.class.name}: " + self.class.field_names.map { |f| "#{f}: " + send(f).inspect }.join(", ") + ">"
179
+ end
180
+
181
+ def inspect; to_s end
182
+
183
+ ## depth-first search on all reachable ModelObjects. fuck yeah.
184
+ def each_modelobject
185
+ seen = {}
186
+ to_see = [self]
187
+ until to_see.empty?
188
+ cur = to_see.pop
189
+ seen[cur] = true
190
+ yield cur
191
+ cur.class.field_names.each do |f|
192
+ val = cur.send(f)
193
+ next if seen[val]
194
+ if val.is_a?(ModelObject)
195
+ to_see.push val
196
+ elsif val.is_a?(Array)
197
+ to_see += val.select { |v| v.is_a?(ModelObject) }
198
+ end
199
+ end
200
+ end
201
+ end
202
+
203
+ def save! fn
204
+ #FileUtils.mv fn, "#{fn}~", :force => true rescue nil
205
+ File.open(fn, "w") { |f| f.puts to_yaml }
206
+ self
207
+ end
208
+
209
+ def to_yaml opts={}
210
+ ret = YAML::quick_emit(object_id, opts) do |out|
211
+ out.map(taguri, nil) do |map|
212
+ self.class.fields.each do |f, fops|
213
+ v = if @serialized_values.member?(f)
214
+ @serialized_values[f]
215
+ else
216
+ @serialized_values[f] = serialized_form_of f, @values[f]
217
+ end
218
+
219
+ map.add f.to_s, v
220
+ end
221
+ end
222
+ end
223
+ YamlWaml.decode(ret)
224
+ end
225
+
226
+ def log what, who, comment
227
+ add_log_event([Time.now, who, what, comment || ""])
228
+ self
229
+ end
230
+
231
+ def changed?; @changed ||= false end
232
+ def changed!; @changed = true end
233
+ def unchanged!; @changed = false end
234
+
235
+ class << self
236
+ ## creates the object, prompting the user when necessary. can take
237
+ ## a :with => { hash } parameter for pre-filling model fields.
238
+ ##
239
+ ## can also take a :defaults_from => obj parameter for pre-filling model
240
+ ## fields from another object with (some of) those fields. kinda like a
241
+ ## bizarre interactive copy constructor.
242
+ def create_interactively opts={}
243
+ o = self.new
244
+ generator_args = opts[:args] || []
245
+ @fields.each do |name, field_opts|
246
+ val = if opts[:with] && opts[:with][name]
247
+ opts[:with][name]
248
+ elsif(found, v = generate_field_value(o, field_opts, generator_args, :interactive => true)) && found
249
+ v
250
+ else
251
+ q = field_opts[:prompt] || name.to_s.capitalize
252
+ if field_opts[:multiline]
253
+ ## multiline options currently aren't allowed to have a default
254
+ ## value, so just ask.
255
+ ask_multiline_smartly q
256
+ else
257
+ default = if opts[:defaults_from] && opts[:defaults_from].respond_to?(name) && (x = opts[:defaults_from].send(name))
258
+ x
259
+ else
260
+ default = generate_field_default o, field_opts, generator_args
261
+ end
262
+ ask q, :default => default
263
+ end
264
+ end
265
+ o.send "#{name}=", val
266
+ end
267
+ o
268
+ end
269
+
270
+ ## creates the object, filling in fields from 'vals', and throwing a
271
+ ## ModelError when it can't find all the requisite fields
272
+ def create vals={}, generator_args=[]
273
+ o = self.new
274
+ @fields.each do |fname, fopts|
275
+ val = if(x = vals[fname] || vals[fname.to_s])
276
+ x
277
+ elsif(found, x = generate_field_value(o, fopts, generator_args, :interactive => false)) && found
278
+ x
279
+ elsif !fopts[:nil_ok]
280
+ raise ModelError, "missing required field #{fname.inspect} on #{self.name} object (got #{vals.keys.inspect})"
281
+ end
282
+ o.send "#{fname}=", val if val
283
+ end
284
+ o
285
+ end
286
+
287
+ private
288
+
289
+ ## get the value for a field if it can be automatically determined
290
+ ## returns [success, value] (because a successful value can be nil)
291
+ def generate_field_value o, field_opts, args, opts={}
292
+ gen = if opts[:interactive]
293
+ field_opts[:interactive_generator] || field_opts[:generator]
294
+ else
295
+ field_opts[:generator]
296
+ end
297
+
298
+ if gen.is_a? Proc
299
+ [true, gen.call(*args)]
300
+ elsif gen
301
+ [true, o.send(gen, *args)]
302
+ elsif field_opts[:ask] == false # nil counts as true here
303
+ [true, field_opts[:default] || (field_opts[:multi] ? [] : nil)]
304
+ else
305
+ [false, nil]
306
+ end
307
+ end
308
+
309
+ def generate_field_default o, field_opts, args
310
+ if field_opts[:default_generator].is_a? Proc
311
+ field_opts[:default_generator].call(*args)
312
+ elsif field_opts[:default_generator]
313
+ o.send field_opts[:default_generator], *args
314
+ elsif field_opts[:default]
315
+ field_opts[:default]
316
+ end
317
+ end
318
+ end
319
+ end
320
+
321
+ end
@@ -134,8 +134,9 @@ class Operator
134
134
  if opts[:cow]
135
135
  puts "MOO!"
136
136
  puts "All is well with the world now. A bit more methane though."
137
- exit 0
137
+ return
138
138
  end
139
+ run_pager
139
140
  return help_single(command) if command
140
141
  puts <<EOS
141
142
  Ditz commands:
@@ -170,14 +171,35 @@ Usage: ditz #{name} #{args}
170
171
  EOS
171
172
  end
172
173
 
173
- operation :add, "Add an issue"
174
- def add project, config
174
+ ## gets a comment from the user, assuming the standard argument setup
175
+ def get_comment opts
176
+ comment = if opts[:no_comment]
177
+ nil
178
+ elsif opts[:comment]
179
+ opts[:comment]
180
+ else
181
+ ask_multiline_smartly "Comments"
182
+ end
183
+ end
184
+ private :get_comment
185
+
186
+ operation :reconfigure, "Rerun configuration script"
187
+ def reconfigure project, config
188
+ new_config = Config.create_interactively :defaults_from => config
189
+ new_config.save! $opts[:config_file]
190
+ puts "Configuration written."
191
+ end
192
+
193
+ operation :add, "Add an issue" do
194
+ opt :comment, "Specify a comment", :short => 'm', :type => String
195
+ opt :no_comment, "Skip asking for a comment", :default => false
196
+ end
197
+ def add project, config, opts
175
198
  issue = Issue.create_interactively(:args => [config, project]) or return
176
- comment = ask_multiline "Comments" unless $opts[:no_comment]
177
- issue.log "created", config.user, comment
199
+ issue.log "created", config.user, get_comment(opts)
178
200
  project.add_issue issue
179
201
  project.assign_issue_names!
180
- puts "Added issue #{issue.name}."
202
+ puts "Added issue #{issue.name} (#{issue.id})."
181
203
  end
182
204
 
183
205
  operation :drop, "Drop an issue", :issue
@@ -186,12 +208,14 @@ EOS
186
208
  puts "Dropped #{issue.name}. Note that other issue names may have changed."
187
209
  end
188
210
 
189
- operation :add_release, "Add a release", :maybe_name
190
- def add_release project, config, maybe_name
211
+ operation :add_release, "Add a release", :maybe_name do
212
+ opt :comment, "Specify a comment", :short => 'm', :type => String
213
+ opt :no_comment, "Skip asking for a comment", :default => false
214
+ end
215
+ def add_release project, config, opts, maybe_name
191
216
  puts "Adding release #{maybe_name}." if maybe_name
192
217
  release = Release.create_interactively(:args => [project, config], :with => { :name => maybe_name }) or return
193
- comment = ask_multiline "Comments" unless $opts[:no_comment]
194
- release.log "created", config.user, comment
218
+ release.log "created", config.user, get_comment(opts)
195
219
  project.add_release release
196
220
  puts "Added release #{release.name}."
197
221
  end
@@ -203,18 +227,21 @@ EOS
203
227
  puts "Added component #{component.name}."
204
228
  end
205
229
 
206
- operation :add_reference, "Add a reference to an issue", :issue
207
- def add_reference project, config, issue
230
+ operation :add_reference, "Add a reference to an issue", :issue do
231
+ opt :comment, "Specify a comment", :short => 'm', :type => String
232
+ opt :no_comment, "Skip asking for a comment", :default => false
233
+ end
234
+ def add_reference project, config, opts, issue
208
235
  puts "Adding a reference to #{issue.name}: #{issue.title}."
209
236
  reference = ask "Reference"
210
- comment = ask_multiline "Comments" unless $opts[:no_comment]
211
237
  issue.add_reference reference
212
- issue.log "added reference #{issue.references.size}", config.user, comment
238
+ issue.log "added reference #{issue.references.size}", config.user, get_comment(opts)
213
239
  puts "Added reference to #{issue.name}."
214
240
  end
215
241
 
216
242
  operation :status, "Show project status", :maybe_release
217
243
  def status project, config, releases
244
+ run_pager
218
245
  releases ||= project.unreleased_releases + [:unassigned]
219
246
 
220
247
  if releases.empty?
@@ -233,7 +260,9 @@ EOS
233
260
  "%2d/%2d %s" % [nc, num, type.to_s.pluralize(num, false)]
234
261
  end
235
262
 
236
- bar = if r != :unassigned && r.released?
263
+ bar = if r == :unassigned
264
+ ""
265
+ elsif r.released?
237
266
  "(released)"
238
267
  elsif issues.empty?
239
268
  "(no issues)"
@@ -273,23 +302,25 @@ EOS
273
302
  join
274
303
  end
275
304
 
276
- def todo_list_for issues
305
+ def todo_list_for issues, opts={}
277
306
  return if issues.empty?
278
307
  name_len = issues.max_of { |i| i.name.length }
279
308
  issues.map do |i|
280
- sprintf "%s %#{name_len}s: %s\n", i.status_widget, i.name, i.title
309
+ s = sprintf "%s %#{name_len}s: %s", i.status_widget, i.name, i.title
310
+ s += " [#{i.release}]" if opts[:show_release] && i.release
311
+ s + "\n"
281
312
  end.join
282
313
  end
283
314
 
284
315
  def print_todo_list_by_release_for project, issues
285
316
  by_release = issues.inject({}) do |h, i|
286
- r = project.release_for i.release
317
+ r = project.release_for(i.release) || :unassigned
287
318
  h[r] ||= []
288
319
  h[r] << i
289
320
  h
290
321
  end
291
322
 
292
- project.releases.each do |r|
323
+ (project.releases + [:unassigned]).each do |r|
293
324
  next unless by_release.member? r
294
325
  puts r == :unassigned ? "Unassigned:" : "#{r.name} (#{r.status}):"
295
326
  print todo_list_for(by_release[r])
@@ -297,17 +328,15 @@ EOS
297
328
  end
298
329
  end
299
330
 
300
- operation :todo, "Generate todo list", :maybe_release
301
- def todo project, config, releases
302
- actually_do_todo project, config, releases, false
331
+ operation :todo, "Generate todo list", :maybe_release do
332
+ opt :all, "Show all issues, included completed ones", :default => false
303
333
  end
304
-
305
- operation :todo_full, "Generate full todo list, including completed items", :maybe_release
306
- def todo_full project, config, releases
307
- actually_do_todo project, config, releases, true
334
+ def todo project, config, opts, releases
335
+ actually_do_todo project, config, releases, opts[:all]
308
336
  end
309
337
 
310
338
  def actually_do_todo project, config, releases, full
339
+ run_pager
311
340
  releases ||= project.unreleased_releases + [:unassigned]
312
341
  releases = [*releases]
313
342
  releases.each do |r|
@@ -324,33 +353,42 @@ EOS
324
353
  ScreenView.new(project, config).render_issue issue
325
354
  end
326
355
 
327
- operation :start, "Start work on an issue", :unstarted_issue
328
- def start project, config, issue
356
+ operation :start, "Start work on an issue", :unstarted_issue do
357
+ opt :comment, "Specify a comment", :short => 'm', :type => String
358
+ opt :no_comment, "Skip asking for a comment", :default => false
359
+ end
360
+ def start project, config, opts, issue
329
361
  puts "Starting work on issue #{issue.name}: #{issue.title}."
330
- comment = ask_multiline "Comments" unless $opts[:no_comment]
331
- issue.start_work config.user, comment
362
+ issue.start_work config.user, get_comment(opts)
332
363
  puts "Recorded start of work for #{issue.name}."
333
364
  end
334
365
 
335
- operation :stop, "Stop work on an issue", :started_issue
336
- def stop project, config, issue
366
+ operation :stop, "Stop work on an issue", :started_issue do
367
+ opt :comment, "Specify a comment", :short => 'm', :type => String
368
+ opt :no_comment, "Skip asking for a comment", :default => false
369
+ end
370
+ def stop project, config, opts, issue
337
371
  puts "Stopping work on issue #{issue.name}: #{issue.title}."
338
- comment = ask_multiline "Comments" unless $opts[:no_comment]
339
- issue.stop_work config.user, comment
372
+ issue.stop_work config.user, get_comment(opts)
340
373
  puts "Recorded work stop for #{issue.name}."
341
374
  end
342
375
 
343
- operation :close, "Close an issue", :open_issue
344
- def close project, config, issue
376
+ operation :close, "Close an issue", :open_issue do
377
+ opt :comment, "Specify a comment", :short => 'm', :type => String
378
+ opt :no_comment, "Skip asking for a comment", :default => false
379
+ end
380
+ def close project, config, opts, issue
345
381
  puts "Closing issue #{issue.name}: #{issue.title}."
346
382
  disp = ask_for_selection Issue::DISPOSITIONS, "disposition", lambda { |x| Issue::DISPOSITION_STRINGS[x] || x.to_s }
347
- comment = ask_multiline "Comments" unless $opts[:no_comment]
348
- issue.close disp, config.user, comment
383
+ issue.close disp, config.user, get_comment(opts)
349
384
  puts "Closed issue #{issue.name} with disposition #{issue.disposition_string}."
350
385
  end
351
386
 
352
- operation :assign, "Assign an issue to a release", :issue, :maybe_release
353
- def assign project, config, issue, maybe_release
387
+ operation :assign, "Assign an issue to a release", :issue, :maybe_release do
388
+ opt :comment, "Specify a comment", :short => 'm', :type => String
389
+ opt :no_comment, "Skip asking for a comment", :default => false
390
+ end
391
+ def assign project, config, opts, issue, maybe_release
354
392
  if maybe_release && maybe_release.name == issue.release
355
393
  raise Error, "issue #{issue.name} already assigned to release #{issue.release}"
356
394
  end
@@ -361,8 +399,6 @@ EOS
361
399
  "not assigned to any release."
362
400
  end
363
401
 
364
- puts "Assigning to release #{maybe_release.name}." if maybe_release
365
-
366
402
  release = maybe_release || begin
367
403
  releases = project.releases.sort_by { |r| (r.release_time || 0).to_i }
368
404
  releases -= [releases.find { |r| r.name == issue.release }] if issue.release
@@ -374,13 +410,15 @@ EOS
374
410
  end
375
411
  end
376
412
  end
377
- comment = ask_multiline "Comments" unless $opts[:no_comment]
378
- issue.assign_to_release release, config.user, comment
413
+ issue.assign_to_release release, config.user, get_comment(opts)
379
414
  puts "Assigned #{issue.name} to #{release.name}."
380
415
  end
381
416
 
382
- operation :set_component, "Set an issue's component", :issue, :maybe_component
383
- def set_component project, config, issue, maybe_component
417
+ operation :set_component, "Set an issue's component", :issue, :maybe_component do
418
+ opt :comment, "Specify a comment", :short => 'm', :type => String
419
+ opt :no_comment, "Skip asking for a comment", :default => false
420
+ end
421
+ def set_component project, config, opts, issue, maybe_component
384
422
  puts "Changing the component of issue #{issue.name}: #{issue.title}."
385
423
 
386
424
  if project.components.size == 1
@@ -396,8 +434,7 @@ EOS
396
434
  components -= [components.find { |r| r.name == issue.component }] if issue.component
397
435
  ask_for_selection(components, "component") { |r| r.name }
398
436
  end
399
- comment = ask_multiline "Comments" unless $opts[:no_comment]
400
- issue.assign_to_component component, config.user, comment
437
+ issue.assign_to_component component, config.user, get_comment(opts)
401
438
  oldname = issue.name
402
439
  project.assign_issue_names!
403
440
  puts <<EOS
@@ -406,18 +443,23 @@ have changed as well.
406
443
  EOS
407
444
  end
408
445
 
409
- operation :unassign, "Unassign an issue from any releases", :assigned_issue
410
- def unassign project, config, issue
446
+ operation :unassign, "Unassign an issue from any releases", :assigned_issue do
447
+ opt :comment, "Specify a comment", :short => 'm', :type => String
448
+ opt :no_comment, "Skip asking for a comment", :default => false
449
+ end
450
+ def unassign project, config, opts, issue
411
451
  puts "Unassigning issue #{issue.name}: #{issue.title}."
412
- comment = ask_multiline "Comments" unless $opts[:no_comment]
413
- issue.unassign config.user, comment
452
+ issue.unassign config.user, get_comment(opts)
414
453
  puts "Unassigned #{issue.name}."
415
454
  end
416
455
 
417
- operation :comment, "Comment on an issue", :issue
418
- def comment project, config, issue
456
+ operation :comment, "Comment on an issue", :issue do
457
+ opt :comment, "Specify a comment", :short => 'm', :type => String
458
+ opt :no_comment, "Skip asking for a comment", :default => false
459
+ end
460
+ def comment project, config, opts, issue
419
461
  puts "Commenting on issue #{issue.name}: #{issue.title}."
420
- comment = ask_multiline "Comments"
462
+ comment = get_comment opts
421
463
  if comment.blank?
422
464
  puts "Empty comment, aborted."
423
465
  else
@@ -428,6 +470,7 @@ EOS
428
470
 
429
471
  operation :releases, "Show releases"
430
472
  def releases project, config
473
+ run_pager
431
474
  a, b = project.releases.partition { |r| r.released? }
432
475
  (b + a.sort_by { |r| r.release_time }).each do |r|
433
476
  status = r.released? ? "released #{r.release_time.pretty_date}" : r.status
@@ -435,15 +478,18 @@ EOS
435
478
  end
436
479
  end
437
480
 
438
- operation :release, "Release a release", :unreleased_release
439
- def release project, config, release
440
- comment = ask_multiline "Comments" unless $opts[:no_comment]
441
- release.release! project, config.user, comment
481
+ operation :release, "Release a release", :unreleased_release do
482
+ opt :comment, "Specify a comment", :short => 'm', :type => String
483
+ opt :no_comment, "Skip asking for a comment", :default => false
484
+ end
485
+ def release project, config, opts, release
486
+ release.release! project, config.user, get_comment(opts)
442
487
  puts "Release #{release.name} released!"
443
488
  end
444
489
 
445
490
  operation :changelog, "Generate a changelog for a release", :release
446
491
  def changelog project, config, r
492
+ run_pager
447
493
  puts "== #{r.name} / #{r.released? ? r.release_time.pretty_date : 'unreleased'}"
448
494
  project.group_issues(project.issues_for_release(r)).each do |type, issues|
449
495
  issues.select { |i| i.closed? }.each do |i|
@@ -467,9 +513,13 @@ EOS
467
513
  ## a no-op
468
514
  end
469
515
 
470
- operation :grep, "Show issues matching a string or regular expression", :string
471
- def grep project, config, match
472
- re = /#{match}/
516
+ operation :grep, "Show issues matching a string or regular expression", :string do
517
+ opt :ignore_case, "Ignore case distinctions in both the expression and in the issue data", :default => false
518
+ end
519
+
520
+ def grep project, config, opts, match
521
+ run_pager
522
+ re = Regexp.new match, opts[:ignore_case]
473
523
  issues = project.issues.select do |i|
474
524
  i.title =~ re || i.desc =~ re ||
475
525
  i.log_events.map { |time, who, what, comments| comments }.join(" ") =~ re
@@ -479,6 +529,7 @@ EOS
479
529
 
480
530
  operation :log, "Show recent activity"
481
531
  def log project, config
532
+ run_pager
482
533
  project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
483
534
  flatten_one_level.sort_by { |e| e.first.first }.reverse.
484
535
  each do |(date, author, what, comment), i|
@@ -496,6 +547,7 @@ EOS
496
547
 
497
548
  operation :shortlog, "Show recent activity (short form)"
498
549
  def shortlog project, config
550
+ run_pager
499
551
  project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
500
552
  flatten_one_level.sort_by { |e| e.first.first }.reverse.
501
553
  each do |(date, author, what, comment), i|
@@ -521,8 +573,12 @@ EOS
521
573
  puts "Archived to #{dir}."
522
574
  end
523
575
 
524
- operation :edit, "Edit an issue", :issue
525
- def edit project, config, issue
576
+ operation :edit, "Edit an issue", :issue do
577
+ opt :comment, "Specify a comment", :short => 'm', :type => String
578
+ opt :no_comment, "Skip asking for a comment", :default => false
579
+ opt :silent, "Don't add a log message detailing the change", :default => false
580
+ end
581
+ def edit project, config, opts, issue
526
582
  data = { :title => issue.title, :description => issue.desc,
527
583
  :reporter => issue.reporter }
528
584
 
@@ -533,11 +589,10 @@ EOS
533
589
  return
534
590
  end
535
591
 
536
- comment = ask_multiline "Comments" unless $opts[:no_comment]
537
-
538
592
  begin
539
593
  edits = YAML.load_file fn
540
- if issue.change edits, config.user, comment
594
+ comment = opts[:silent] ? nil : get_comment(opts)
595
+ if issue.change edits, config.user, comment, opts[:silent]
541
596
  puts "Change recorded."
542
597
  else
543
598
  puts "No changes."