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.
- data/Changelog +13 -0
- data/README.txt +6 -6
- data/Rakefile +2 -2
- data/ReleaseNotes +13 -0
- data/bin/ditz +116 -44
- data/bin/ditz-convert-from-monolith +0 -0
- data/lib/ditz.rb +17 -1
- data/lib/hook.rb +60 -0
- data/lib/html.rb +6 -0
- data/lib/index.rhtml +13 -10
- data/lib/issue.rhtml +3 -7
- data/lib/lowline.rb +40 -18
- data/lib/model-objects.rb +58 -18
- data/lib/model.rb +75 -14
- data/lib/operator.rb +182 -69
- data/lib/release.rhtml +1 -1
- data/lib/util.rb +8 -0
- metadata +51 -43
data/lib/operator.rb
CHANGED
@@ -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
|
-
|
39
|
-
|
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
|
-
|
51
|
-
|
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
|
-
|
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
|
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
|
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
|
153
|
-
|
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", :
|
187
|
+
operation :status, "Show project status", :magic_release
|
177
188
|
def status project, config, releases
|
178
|
-
releases.
|
179
|
-
|
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
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
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
|
214
|
+
elsif issues.empty?
|
189
215
|
"(no issues)"
|
190
|
-
elsif
|
216
|
+
elsif issues.all? { |i| i.closed? }
|
191
217
|
"(ready for release)"
|
192
218
|
else
|
193
|
-
|
219
|
+
status_bar_for(issues)
|
194
220
|
end
|
195
221
|
|
196
|
-
|
197
|
-
title, ncbugs, bugs.size, pcbugs, ncfeats, feats.size, pcfeats, special
|
222
|
+
[title, middle, bar]
|
198
223
|
end
|
199
224
|
|
200
|
-
|
201
|
-
|
202
|
-
|
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", :
|
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", :
|
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,
|
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 =
|
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.
|
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
|
-
|
311
|
-
|
312
|
-
release =
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
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
|
-
|
337
|
-
|
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
|
-
|
361
|
-
|
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.
|
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
|
-
|
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
|