ditz 0.2 → 0.3

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.
@@ -34,24 +34,15 @@ class Operator
34
34
 
35
35
  releases.each do |r|
36
36
  next if r.released? unless force_show
37
-
38
- bugs = project.issues.
39
- select { |i| i.type == :bugfix && i.release == r.name }
40
- feats = project.issues.
41
- select { |i| i.type == :feature && i.release == r.name }
42
-
43
- #next if bugs.empty? && feats.empty? unless force_show
44
-
45
- ret << [r, bugs, feats]
37
+ groups = project.group_issues(project.issues_for_release(r))
38
+ #next if groups.empty? unless force_show
39
+ ret << [r, groups]
46
40
  end
47
41
 
48
42
  return ret unless show_unassigned
49
-
50
- bugs = project.issues.select { |i| i.type == :bugfix && i.release.nil? }
51
- feats = project.issues.select { |i| i.type == :feature && i.release.nil? }
52
-
53
- return ret if bugs.empty? && feats.empty? unless force_show
54
- ret << [nil, bugs, feats]
43
+ groups = project.group_issues(project.unassigned_issues)
44
+ return ret if groups.empty? unless force_show
45
+ ret << [nil, groups]
55
46
  end
56
47
  private :parse_releases_arg
57
48
 
@@ -59,14 +50,20 @@ class Operator
59
50
  command = "command '#{method_to_op method}'"
60
51
  built_args = @operations[method][:args_spec].map do |spec|
61
52
  val = args.shift
53
+ generate_choices(project, method, spec) if val == '<options>'
62
54
  case spec
63
55
  when :issue
64
56
  raise Error, "#{command} requires an issue name" unless val
65
- project.issue_for(val) or raise Error, "no issue with name #{val}"
57
+ valr = val.sub(/\A(\w+-\d+)_.*$/,'\1')
58
+ project.issue_for(valr) or raise Error, "no issue with name #{val}"
66
59
  when :release
67
60
  raise Error, "#{command} requires a release name" unless val
68
61
  project.release_for(val) or raise Error, "no release with name #{val}"
69
62
  when :maybe_release
63
+ project.release_for(val) or raise Error, "no release with name #{val}" if val
64
+ when :maybe_component
65
+ project.component_for(val) or raise Error, "no component with name #{val}" if val
66
+ when :magic_release
70
67
  parse_releases_arg project, val
71
68
  when :string
72
69
  raise Error, "#{command} requires a string" unless val
@@ -75,9 +72,20 @@ class Operator
75
72
  val # no translation for other types
76
73
  end
77
74
  end
75
+ generate_choices(project, method, nil) if args.include? '<options>'
78
76
  raise Error, "too many arguments for #{command}" unless args.empty?
79
77
  built_args
80
78
  end
79
+
80
+ def generate_choices project, method, spec
81
+ case spec
82
+ when :issue
83
+ puts project.issues.map { |i| "#{i.name}_#{i.title.gsub(/\W+/, '-')}" }
84
+ when :release, :maybe_release
85
+ puts project.releases.map { |r| r.name }
86
+ end
87
+ exit 0
88
+ end
81
89
  end
82
90
 
83
91
  def do op, project, config, args
@@ -118,6 +126,8 @@ EOS
118
126
  raise Error, "no such ditz command '#{command}'" unless name
119
127
  args = opts[:args_spec].map do |spec|
120
128
  case spec.to_s
129
+ when "magic_release"
130
+ "[release]"
121
131
  when /^maybe_(.*)$/
122
132
  "[#{$1}]"
123
133
  else
@@ -131,26 +141,27 @@ Usage: ditz #{name} #{args}
131
141
  EOS
132
142
  end
133
143
 
134
- operation :add, "Add a bug/feature request"
144
+ operation :add, "Add an issue"
135
145
  def add project, config
136
146
  issue = Issue.create_interactively(:args => [config, project]) or return
137
- comment = ask_multiline "Comments"
147
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
138
148
  issue.log "created", config.user, comment
139
149
  project.add_issue issue
140
150
  project.assign_issue_names!
141
151
  puts "Added issue #{issue.name}."
142
152
  end
143
153
 
144
- operation :drop, "Drop a bug/feature request", :issue
154
+ operation :drop, "Drop an issue", :issue
145
155
  def drop project, config, issue
146
156
  project.drop_issue issue
147
157
  puts "Dropped #{issue.name}. Note that other issue names may have changed."
148
158
  end
149
159
 
150
- operation :add_release, "Add a release"
151
- def add_release project, config
152
- release = Release.create_interactively(:args => [project, config]) or return
153
- comment = ask_multiline "Comments"
160
+ operation :add_release, "Add a release", :maybe_name
161
+ def add_release project, config, maybe_name
162
+ puts "Adding release #{maybe_name}." if maybe_name
163
+ release = Release.create_interactively(:args => [project, config], :with => { :name => maybe_name }) or return
164
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
154
165
  release.log "created", config.user, comment
155
166
  project.add_release release
156
167
  puts "Added release #{release.name}."
@@ -167,39 +178,67 @@ EOS
167
178
  def add_reference project, config, issue
168
179
  puts "Adding a reference to #{issue.name}: #{issue.title}."
169
180
  reference = ask "Reference"
170
- comment = ask_multiline "Comments"
181
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
171
182
  issue.add_reference reference
172
183
  issue.log "added reference #{issue.references.size}", config.user, comment
173
184
  puts "Added reference to #{issue.name}."
174
185
  end
175
186
 
176
- operation :status, "Show project status", :maybe_release
187
+ operation :status, "Show project status", :magic_release
177
188
  def status project, config, releases
178
- releases.each do |r, bugs, feats|
179
- title, bar = [r ? r.name : "unassigned", status_bar_for(bugs + feats)]
189
+ if releases.empty?
190
+ puts "No releases."
191
+ return
192
+ end
193
+
194
+ ## TODO: remove weird and deprecated :maybe_release semantics
195
+ releases = releases.map { |r, groups| r }
180
196
 
181
- ncbugs = bugs.count_of { |b| b.closed? }
182
- ncfeats = feats.count_of { |f| f.closed? }
183
- pcbugs = 100.0 * (bugs.empty? ? 1.0 : ncbugs.to_f / bugs.size)
184
- pcfeats = 100.0 * (feats.empty? ? 1.0 : ncfeats.to_f / feats.size)
197
+ entries = releases.map do |r|
198
+ title, issues = if r
199
+ [r.name, project.issues_for_release(r)]
200
+ else
201
+ ["unassigned", project.unassigned_issues]
202
+ end
185
203
 
186
- special = if r && r.released?
204
+ middle = Issue::TYPES.map do |type|
205
+ type_issues = issues.select { |i| i.type == type }
206
+ num = type_issues.size
207
+ nc = type_issues.count_of { |i| i.closed? }
208
+ pc = 100.0 * (type_issues.empty? ? 1.0 : nc.to_f / num)
209
+ "%2d/%2d %s" % [nc, num, type.to_s.pluralize(num, false)]
210
+ end
211
+
212
+ bar = if r && r.released?
187
213
  "(released)"
188
- elsif bugs.empty? && feats.empty?
214
+ elsif issues.empty?
189
215
  "(no issues)"
190
- elsif ncbugs == bugs.size && ncfeats == feats.size
216
+ elsif issues.all? { |i| i.closed? }
191
217
  "(ready for release)"
192
218
  else
193
- bar
219
+ status_bar_for(issues)
194
220
  end
195
221
 
196
- printf "%-10s %2d/%2d (%3.0f%%) bugs, %2d/%2d (%3.0f%%) features %s\n",
197
- title, ncbugs, bugs.size, pcbugs, ncfeats, feats.size, pcfeats, special
222
+ [title, middle, bar]
198
223
  end
199
224
 
200
- if project.releases.empty?
201
- puts "No releases."
202
- return
225
+ title_size = 0
226
+ middle_sizes = []
227
+
228
+ entries.each do |title, middle, bar|
229
+ title_size = [title_size, title.length].max
230
+ middle_sizes = middle.zip(middle_sizes).map do |e, s|
231
+ [s || 0, e.length].max
232
+ end
233
+ end
234
+
235
+ entries.each do |title, middle, bar|
236
+ printf "%-#{title_size}s ", title
237
+ middle.zip(middle_sizes).each_with_index do |(e, s), i|
238
+ sep = i < middle.size - 1 ? "," : ""
239
+ printf "%-#{s + sep.length}s ", e + sep
240
+ end
241
+ puts bar
203
242
  end
204
243
  end
205
244
 
@@ -218,24 +257,24 @@ EOS
218
257
  end.join
219
258
  end
220
259
 
221
- operation :todo, "Generate todo list", :maybe_release
260
+ operation :todo, "Generate todo list", :magic_release
222
261
  def todo project, config, releases
223
262
  actually_do_todo project, config, releases, false
224
263
  end
225
264
 
226
- operation :todo_full, "Generate full todo list, including completed items", :maybe_release
265
+ operation :todo_full, "Generate full todo list, including completed items", :magic_release
227
266
  def todo_full project, config, releases
228
267
  actually_do_todo project, config, releases, true
229
268
  end
230
269
 
231
270
  def actually_do_todo project, config, releases, full
232
- releases.each do |r, bugs, feats|
271
+ releases.each do |r, groups|
233
272
  if r
234
273
  puts "Version #{r.name} (#{r.status}):"
235
274
  else
236
275
  puts "Unassigned:"
237
276
  end
238
- issues = bugs + feats
277
+ issues = groups.map { |_,g| g }.flatten
239
278
  issues = issues.select { |i| i.open? } unless full
240
279
  puts(todo_list_for(issues.sort_by { |i| i.sort_order }) || "No open issues.")
241
280
  puts
@@ -253,7 +292,7 @@ EOS
253
292
  puts <<EOS
254
293
  #{"Issue #{issue.name}".underline}
255
294
  Title: #{issue.title}
256
- Description: #{issue.interpolated_desc(project.issues).multiline " "}
295
+ Description: #{issue.desc.multiline " "}
257
296
  Type: #{issue.type}
258
297
  Status: #{status}
259
298
  Creator: #{issue.reporter}
@@ -277,7 +316,7 @@ EOS
277
316
  operation :start, "Start work on an issue", :issue
278
317
  def start project, config, issue
279
318
  puts "Starting work on issue #{issue.name}: #{issue.title}."
280
- comment = ask_multiline "Comments"
319
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
281
320
  issue.start_work config.user, comment
282
321
  puts "Recorded start of work for #{issue.name}."
283
322
  end
@@ -285,7 +324,7 @@ EOS
285
324
  operation :stop, "Stop work on an issue", :issue
286
325
  def stop project, config, issue
287
326
  puts "Stopping work on issue #{issue.name}: #{issue.title}."
288
- comment = ask_multiline "Comments"
327
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
289
328
  issue.stop_work config.user, comment
290
329
  puts "Recorded work stop for #{issue.name}."
291
330
  end
@@ -294,37 +333,72 @@ EOS
294
333
  def close project, config, issue
295
334
  puts "Closing issue #{issue.name}: #{issue.title}."
296
335
  disp = ask_for_selection Issue::DISPOSITIONS, "disposition", lambda { |x| Issue::DISPOSITION_STRINGS[x] || x.to_s }
297
- comment = ask_multiline "Comments"
336
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
298
337
  issue.close disp, config.user, comment
299
338
  puts "Closed issue #{issue.name} with disposition #{issue.disposition_string}."
300
339
  end
301
340
 
302
- operation :assign, "Assign an issue to a release", :issue
303
- def assign project, config, issue
341
+ operation :assign, "Assign an issue to a release", :issue, :maybe_release
342
+ def assign project, config, issue, maybe_release
343
+ if maybe_release && maybe_release.name == issue.release
344
+ raise Error, "issue #{issue.name} already assigned to release #{issue.release}"
345
+ end
346
+
304
347
  puts "Issue #{issue.name} currently " + if issue.release
305
348
  "assigned to release #{issue.release}."
306
349
  else
307
350
  "not assigned to any release."
308
351
  end
309
352
 
310
- releases = project.releases.sort_by { |r| (r.release_time || 0).to_i }
311
- releases -= [releases.find { |r| r.name == issue.release }] if issue.release
312
- release = ask_for_selection(releases, "release") do |r|
313
- r.name + if r.released?
314
- " (released #{r.release_time.pretty_date})"
315
- else
316
- " (unreleased)"
353
+ puts "Assigning to release #{maybe_release.name}." if maybe_release
354
+
355
+ release = maybe_release || begin
356
+ releases = project.releases.sort_by { |r| (r.release_time || 0).to_i }
357
+ releases -= [releases.find { |r| r.name == issue.release }] if issue.release
358
+ ask_for_selection(releases, "release") do |r|
359
+ r.name + if r.released?
360
+ " (released #{r.release_time.pretty_date})"
361
+ else
362
+ " (unreleased)"
363
+ end
317
364
  end
318
365
  end
319
- comment = ask_multiline "Comments"
366
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
320
367
  issue.assign_to_release release, config.user, comment
321
368
  puts "Assigned #{issue.name} to #{release.name}."
322
369
  end
323
370
 
371
+ operation :set_component, "Set an issue's component", :issue, :maybe_component
372
+ def set_component project, config, issue, maybe_component
373
+ puts "Changing the component of issue #{issue.name}: #{issue.title}."
374
+
375
+ if project.components.size == 1
376
+ raise Error, "this project does not use multiple components"
377
+ end
378
+
379
+ if maybe_component && maybe_component.name == issue.component
380
+ raise Error, "issue #{issue.name} already assigned to component #{issue.component}"
381
+ end
382
+
383
+ component = maybe_component || begin
384
+ components = project.components
385
+ components -= [components.find { |r| r.name == issue.component }] if issue.component
386
+ ask_for_selection(components, "component") { |r| r.name }
387
+ end
388
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
389
+ issue.assign_to_component component, config.user, comment
390
+ oldname = issue.name
391
+ project.assign_issue_names!
392
+ puts <<EOS
393
+ Issue #{oldname} is now #{issue.name}. Note that the names of other issues may
394
+ have changed as well.
395
+ EOS
396
+ end
397
+
324
398
  operation :unassign, "Unassign an issue from any releases", :issue
325
399
  def unassign project, config, issue
326
400
  puts "Unassigning issue #{issue.name}: #{issue.title}."
327
- comment = ask_multiline "Comments"
401
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
328
402
  issue.unassign config.user, comment
329
403
  puts "Unassigned #{issue.name}."
330
404
  end
@@ -333,8 +407,12 @@ EOS
333
407
  def comment project, config, issue
334
408
  puts "Commenting on issue #{issue.name}: #{issue.title}."
335
409
  comment = ask_multiline "Comments"
336
- issue.log "commented", config.user, comment
337
- puts "Comments recorded for #{issue.name}."
410
+ if comment.blank?
411
+ puts "Empty comment, aborted."
412
+ else
413
+ issue.log "commented", config.user, comment
414
+ puts "Comments recorded for #{issue.name}."
415
+ end
338
416
  end
339
417
 
340
418
  operation :releases, "Show releases"
@@ -348,17 +426,23 @@ EOS
348
426
 
349
427
  operation :release, "Release a release", :release
350
428
  def release project, config, release
351
- comment = ask_multiline "Comments"
429
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
352
430
  release.release! project, config.user, comment
353
431
  puts "Release #{release.name} released!"
354
432
  end
355
433
 
356
434
  operation :changelog, "Generate a changelog for a release", :release
357
435
  def changelog project, config, r
358
- feats, bugs = project.issues_for_release(r).partition { |i| i.feature? }
359
436
  puts "== #{r.name} / #{r.released? ? r.release_time.pretty_date : 'unreleased'}"
360
- feats.select { |f| f.closed? }.each { |i| puts "* #{i.title}" }
361
- bugs.select { |f| f.closed? }.each { |i| puts "* bugfix: #{i.title}" }
437
+ project.group_issues(project.issues_for_release(r)).each do |type, issues|
438
+ issues.select { |i| i.closed? }.each do |i|
439
+ if type == :bugfix
440
+ puts "* #{type}: #{i.title}"
441
+ else
442
+ puts "* #{i.title}"
443
+ end
444
+ end
445
+ end
362
446
  end
363
447
 
364
448
  operation :html, "Generate html status pages", :maybe_dir
@@ -368,7 +452,9 @@ EOS
368
452
 
369
453
  ## find the ERB templates. this is my brilliant approach
370
454
  ## to the 'gem datadir' problem.
371
- template_dir = $:.find { |p| File.exists? File.join(p, "index.rhtml") }
455
+ template_dir = $:.find { |p| File.exist? File.expand_path(File.join(p, "index.rhtml")) }
456
+ raise "can't find index.rhtml in any path" unless template_dir
457
+ template_dir = File.expand_path template_dir
372
458
 
373
459
  FileUtils.cp File.join(template_dir, "style.css"), dir
374
460
 
@@ -447,15 +533,42 @@ EOS
447
533
  puts <<EOS
448
534
  date : #{date.localtime} (#{date.ago} ago)
449
535
  author: #{author}
536
+ issue: [#{i.name}] #{i.title}
450
537
 
451
- #{i.name}: #{i.title}
452
538
  #{what}
453
- #{comment.multiline " "}
539
+ #{comment.multiline " > ", false}
454
540
  EOS
455
541
  puts unless comment.blank?
456
542
  end
457
543
  end
458
544
 
545
+ operation :shortlog, "Show recent activity (short form)"
546
+ def shortlog project, config
547
+ project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
548
+ flatten_one_level.sort_by { |e| e.first.first }.reverse.
549
+ each do |(date, author, what, comment), i|
550
+ shortauthor = if author =~ /<(.*?)@/
551
+ $1
552
+ else
553
+ author
554
+ end[0..15]
555
+ printf "%13s|%13s|%13s|%s\n", date.ago, i.name, shortauthor,
556
+ what
557
+ end
558
+ end
559
+
560
+ operation :archive, "Archive a release", :release, :maybe_dir
561
+ def archive project, config, release, dir
562
+ dir ||= "ditz-archive-#{release.name}"
563
+ FileUtils.mkdir dir
564
+ FileUtils.cp project.pathname, dir
565
+ project.issues_for_release(release).each do |i|
566
+ FileUtils.cp i.pathname, dir
567
+ project.drop_issue i
568
+ end
569
+ puts "Archived to #{dir}."
570
+ end
571
+
459
572
  operation :edit, "Edit an issue", :issue
460
573
  def edit project, config, issue
461
574
  data = { :title => issue.title, :description => issue.desc,
@@ -468,7 +581,7 @@ EOS
468
581
  return
469
582
  end
470
583
 
471
- comment = ask_multiline "Comments"
584
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
472
585
 
473
586
  begin
474
587
  edits = YAML.load_file fn
@@ -55,7 +55,7 @@
55
55
  <tr><td colspan="3" class="logcomment">
56
56
  <% if comment.empty? %>
57
57
  <% else %>
58
- <%=p comment %>
58
+ <%= link_issue_names project, p(comment) %>
59
59
  <% end %>
60
60
  </td></tr>
61
61
  <% end %>