ditz 0.3 → 0.4

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.
@@ -73,8 +73,13 @@
73
73
  <%= issue.status_string %><% if issue.closed? %>: <%= issue.disposition_string %><% end %>
74
74
  </td>
75
75
  </tr>
76
+
77
+ <%= extra_summary_html %>
78
+
76
79
  </table>
77
80
 
81
+ <%= extra_details_html %>
82
+
78
83
  <h2>Issue log</h2>
79
84
 
80
85
  <table>
@@ -84,7 +89,7 @@
84
89
  <% else %>
85
90
  <tr class="logentryodd">
86
91
  <% end %>
87
- <td class="logtime"><%=h time %></td>
92
+ <td class="logtime"><%=t time %></td>
88
93
  <td class="logwho"><%=obscured_email who %></td>
89
94
  <td class="logwhat"><%=h what %></td>
90
95
  </tr>
@@ -2,10 +2,14 @@
2
2
  <% issues.sort_by { |i| i.sort_order }.each do |i| %>
3
3
  <tr>
4
4
  <td class="issuestatus_<%= i.status %>">
5
- <%= i.status_string %><% if i.closed? %>: <%= i.disposition_string %><% end %>
5
+ <% if i.closed? %>
6
+ <%= i.disposition_string %>
7
+ <% else %>
8
+ <%= i.status_string %>
9
+ <% end %>
6
10
  </td>
7
11
  <td class="issuename">
8
- <%= link_to i, i.title %>
12
+ <%= fancy_issue_link_for i %>
9
13
  <%= i.bug? ? '(bug)' : '' %>
10
14
  </td>
11
15
  <% if show_release %>
@@ -7,22 +7,15 @@ class Numeric
7
7
  end
8
8
  end
9
9
 
10
- class NilClass
11
- def multiline prefix=nil; "" end
12
- end
13
-
14
10
  class String
15
11
  def dcfirst; self[0..0].downcase + self[1..-1] end
16
12
  def blank?; self =~ /\A\s*\z/ end
17
13
  def underline; self + "\n" + ("-" * self.length) end
18
- def multiline prefix="", cleanstart=true
19
- return "" if blank?
20
- (cleanstart ? "\n" : "") + gsub(/^/, prefix)
21
- end
22
14
  def pluralize n, b=true
23
15
  s = (n == 1 ? self : (self == 'bugfix' ? 'bugfixes' : self + "s")) # oh yeah
24
16
  b ? n.to_pretty_s + " " + s : s
25
17
  end
18
+ def shortened_email; self =~ /<?(\S+?)@.+/ ? $1 : self end
26
19
  def multistrip; strip.gsub(/\n\n+/, "\n\n") end
27
20
  end
28
21
 
@@ -73,8 +73,10 @@ EOS
73
73
  releases.find { |i| i.name == release_name }
74
74
  end
75
75
 
76
+ def unreleased_releases; releases.select { |r| r.unreleased? } end
77
+
76
78
  def issues_for_release release
77
- issues.select { |i| i.release == release.name }
79
+ release == :unassigned ? unassigned_issues : issues.select { |i| i.release == release.name }
78
80
  end
79
81
 
80
82
  def issues_for_component component
@@ -186,8 +188,11 @@ class Issue < ModelObject
186
188
  def closed?; status == :closed end
187
189
  def open?; !closed? end
188
190
  def in_progress?; status == :in_progress end
191
+ def unstarted?; !in_progress? end
189
192
  def bug?; type == :bugfix end
190
193
  def feature?; type == :feature end
194
+ def unassigned?; release.nil? end
195
+ def assigned?; !unassigned? end
191
196
 
192
197
  def start_work who, comment; change_status :in_progress, who, comment end
193
198
  def stop_work who, comment
@@ -197,7 +202,7 @@ class Issue < ModelObject
197
202
 
198
203
  def close disp, who, comment
199
204
  raise Error, "unknown disposition #{disp}" unless DISPOSITIONS.member? disp
200
- log "closed issue with disposition #{disp}", who, comment
205
+ log "closed with disposition #{disp}", who, comment
201
206
  self.status = :closed
202
207
  self.disposition = disp
203
208
  end
@@ -18,6 +18,7 @@ class ModelObject
18
18
  def initialize
19
19
  @values = {}
20
20
  @serialized_values = {}
21
+ self.class.fields.map { |f, opts| @values[f] = [] if opts[:multi] }
21
22
  end
22
23
 
23
24
  ## yamlability
@@ -27,7 +28,14 @@ class ModelObject
27
28
  def self.inherited subclass
28
29
  YAML.add_domain_type(yaml_domain, subclass.yaml_other_thing) do |type, val|
29
30
  o = subclass.new
30
- val.each { |k, v| o.send "__serialized_#{k}=", v }
31
+ val.each do |k, v|
32
+ m = "__serialized_#{k}="
33
+ if o.respond_to? m
34
+ o.send m, v
35
+ else
36
+ $stderr.puts "warning: unknown field #{k.inspect} in YAML for #{type}; ignoring"
37
+ end
38
+ end
31
39
  o.unchanged!
32
40
  o
33
41
  end
@@ -137,8 +145,8 @@ class ModelObject
137
145
  self
138
146
  end
139
147
 
140
- def to_yaml opts={}
141
- YAML::quick_emit(object_id, opts) do |out|
148
+ def to_yaml opts={}
149
+ YAML::quick_emit(object_id, opts) do |out|
142
150
  out.map(taguri, nil) do |map|
143
151
  self.class.fields.each do |f, fops|
144
152
  v = if @serialized_values.member?(f)
@@ -151,7 +159,7 @@ class ModelObject
151
159
  end
152
160
  end
153
161
  end
154
- end
162
+ end
155
163
 
156
164
  def log what, who, comment
157
165
  add_log_event([Time.now, who, what, comment || ""])
@@ -1,5 +1,4 @@
1
1
  require 'fileutils'
2
- require "html"
3
2
 
4
3
  module Ditz
5
4
 
@@ -20,72 +19,79 @@ class Operator
20
19
  end
21
20
  def has_operation? op; @operations.member? op_to_method(op) end
22
21
 
23
- def parse_releases_arg project, releases_arg
24
- ret = []
25
-
26
- releases, show_unassigned, force_show = case releases_arg
27
- when nil; [project.releases, true, false]
28
- when "unassigned"; [[], true, true]
29
- else
30
- release = project.release_for(releases_arg)
31
- raise Error, "no release with name #{releases_arg}" unless release
32
- [[release], false, true]
33
- end
22
+ ## parse the specs, and the commandline arguments, and resolve them. does
23
+ ## typechecking but currently doesn't check for open_issues actually being
24
+ ## open, unstarted_issues being unstarted, etc. probably will check for
25
+ ## this in the future.
26
+ def build_args project, method, args
27
+ specs = @operations[method][:args_spec]
28
+ command = "command '#{method_to_op method}'"
34
29
 
35
- releases.each do |r|
36
- next if r.released? unless force_show
37
- groups = project.group_issues(project.issues_for_release(r))
38
- #next if groups.empty? unless force_show
39
- ret << [r, groups]
30
+ if specs.empty? && args == ["<options>"]
31
+ die_with_completions project, method, nil
40
32
  end
41
33
 
42
- return ret unless show_unassigned
43
- groups = project.group_issues(project.unassigned_issues)
44
- return ret if groups.empty? unless force_show
45
- ret << [nil, groups]
46
- end
47
- private :parse_releases_arg
48
-
49
- def build_args project, method, args
50
- command = "command '#{method_to_op method}'"
51
- built_args = @operations[method][:args_spec].map do |spec|
34
+ built_args = specs.map do |spec|
35
+ optional = spec.to_s =~ /^maybe_/
36
+ spec = spec.to_s.gsub(/^maybe_/, "").intern # :(
52
37
  val = args.shift
53
- generate_choices(project, method, spec) if val == '<options>'
38
+
39
+ case val
40
+ when nil
41
+ next if optional
42
+ specname = spec.to_s.gsub("_", " ")
43
+ article = specname =~ /^[aeiou]/ ? "an" : "a"
44
+ raise Error, "#{command} requires #{article} #{specname}"
45
+ when "<options>"
46
+ die_with_completions project, method, spec
47
+ end
48
+
54
49
  case spec
55
- when :issue
56
- raise Error, "#{command} requires an issue name" unless val
50
+ when :issue, :open_issue, :unstarted_issue, :started_issue, :assigned_issue
51
+ ## issue completion sticks the title on there, so this will strip it off
57
52
  valr = val.sub(/\A(\w+-\d+)_.*$/,'\1')
58
53
  project.issue_for(valr) or raise Error, "no issue with name #{val}"
59
- when :release
60
- raise Error, "#{command} requires a release name" unless val
61
- project.release_for(val) or raise Error, "no release with name #{val}"
62
- when :maybe_release
63
- project.release_for(val) or raise Error, "no release with name #{val}" if val
64
- when :maybe_component
54
+ when :release, :unreleased_release
55
+ if val == "unassigned"
56
+ :unassigned
57
+ else
58
+ project.release_for(val) or raise Error, "no release with name #{val}"
59
+ end
60
+ when :component
65
61
  project.component_for(val) or raise Error, "no component with name #{val}" if val
66
- when :magic_release
67
- parse_releases_arg project, val
68
- when :string
69
- raise Error, "#{command} requires a string" unless val
70
- val
71
62
  else
72
63
  val # no translation for other types
73
64
  end
74
65
  end
75
- generate_choices(project, method, nil) if args.include? '<options>'
66
+
76
67
  raise Error, "too many arguments for #{command}" unless args.empty?
77
68
  built_args
78
69
  end
79
70
 
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
71
+ def die_with_completions project, method, spec
72
+ puts(case spec
73
+ when :issue, :open_issue, :unstarted_issue, :started_issue, :assigned_issue
74
+ m = { :issue => nil,
75
+ :open_issue => :open?,
76
+ :unstarted_issue => :unstarted?,
77
+ :started_issue => :in_progress?,
78
+ :assigned_issue => :assigned?,
79
+ }[spec]
80
+ project.issues.select { |i| m.nil? || i.send(m) }.sort_by { |i| i.creation_time }.reverse.map { |i| "#{i.name}_#{i.title.gsub(/\W+/, '-')}" }
81
+ when :release
82
+ project.releases.map { |r| r.name } + ["unassigned"]
83
+ when :unreleased_release
84
+ project.releases.select { |r| r.unreleased? }.map { |r| r.name }
85
+ when :component
86
+ project.components.map { |c| c.name }
87
+ when :command
88
+ operations.map { |name, _| name }
89
+ else
90
+ ""
91
+ end)
87
92
  exit 0
88
93
  end
94
+ private :die_with_completions
89
95
  end
90
96
 
91
97
  def do op, project, config, args
@@ -126,8 +132,6 @@ EOS
126
132
  raise Error, "no such ditz command '#{command}'" unless name
127
133
  args = opts[:args_spec].map do |spec|
128
134
  case spec.to_s
129
- when "magic_release"
130
- "[release]"
131
135
  when /^maybe_(.*)$/
132
136
  "[#{$1}]"
133
137
  else
@@ -184,22 +188,17 @@ EOS
184
188
  puts "Added reference to #{issue.name}."
185
189
  end
186
190
 
187
- operation :status, "Show project status", :magic_release
191
+ operation :status, "Show project status", :maybe_release
188
192
  def status project, config, releases
193
+ releases ||= project.unreleased_releases + [:unassigned]
194
+
189
195
  if releases.empty?
190
196
  puts "No releases."
191
197
  return
192
198
  end
193
199
 
194
- ## TODO: remove weird and deprecated :maybe_release semantics
195
- releases = releases.map { |r, groups| r }
196
-
197
200
  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
201
+ title, issues = (r == :unassigned ? r.to_s : r.name), project.issues_for_release(r)
203
202
 
204
203
  middle = Issue::TYPES.map do |type|
205
204
  type_issues = issues.select { |i| i.type == type }
@@ -209,7 +208,7 @@ EOS
209
208
  "%2d/%2d %s" % [nc, num, type.to_s.pluralize(num, false)]
210
209
  end
211
210
 
212
- bar = if r && r.released?
211
+ bar = if r != :unassigned && r.released?
213
212
  "(released)"
214
213
  elsif issues.empty?
215
214
  "(no issues)"
@@ -257,24 +256,26 @@ EOS
257
256
  end.join
258
257
  end
259
258
 
260
- operation :todo, "Generate todo list", :magic_release
259
+ operation :todo, "Generate todo list", :maybe_release
261
260
  def todo project, config, releases
262
261
  actually_do_todo project, config, releases, false
263
262
  end
264
263
 
265
- operation :todo_full, "Generate full todo list, including completed items", :magic_release
264
+ operation :todo_full, "Generate full todo list, including completed items", :maybe_release
266
265
  def todo_full project, config, releases
267
266
  actually_do_todo project, config, releases, true
268
267
  end
269
268
 
270
269
  def actually_do_todo project, config, releases, full
271
- releases.each do |r, groups|
272
- if r
273
- puts "Version #{r.name} (#{r.status}):"
274
- else
270
+ releases ||= project.unreleased_releases + [:unassigned]
271
+ releases = [*releases]
272
+ releases.each do |r|
273
+ if r == :unassigned
275
274
  puts "Unassigned:"
275
+ else
276
+ puts "Version #{r.name} (#{r.status}):"
276
277
  end
277
- issues = groups.map { |_,g| g }.flatten
278
+ issues = project.issues_for_release r
278
279
  issues = issues.select { |i| i.open? } unless full
279
280
  puts(todo_list_for(issues.sort_by { |i| i.sort_order }) || "No open issues.")
280
281
  puts
@@ -283,37 +284,10 @@ EOS
283
284
 
284
285
  operation :show, "Describe a single issue", :issue
285
286
  def show project, config, issue
286
- status = case issue.status
287
- when :closed
288
- "#{issue.status_string}: #{issue.disposition_string}"
289
- else
290
- issue.status_string
291
- end
292
- puts <<EOS
293
- #{"Issue #{issue.name}".underline}
294
- Title: #{issue.title}
295
- Description: #{issue.desc.multiline " "}
296
- Type: #{issue.type}
297
- Status: #{status}
298
- Creator: #{issue.reporter}
299
- Age: #{issue.creation_time.ago}
300
- Release: #{issue.release}
301
- References: #{issue.references.listify " "}
302
- Identifier: #{issue.id}
303
-
304
- Event log:
305
- #{format_log_events issue.log_events}
306
- EOS
287
+ ScreenView.new(project, config).render_issue issue
307
288
  end
308
289
 
309
- def format_log_events events
310
- return "none" if events.empty?
311
- events.map do |time, who, what, comment|
312
- "- #{time.pretty} :: #{who}\n #{what}#{comment.multiline " > "}"
313
- end.join("\n")
314
- end
315
-
316
- operation :start, "Start work on an issue", :issue
290
+ operation :start, "Start work on an issue", :unstarted_issue
317
291
  def start project, config, issue
318
292
  puts "Starting work on issue #{issue.name}: #{issue.title}."
319
293
  comment = ask_multiline "Comments" unless $opts[:no_comment]
@@ -321,7 +295,7 @@ EOS
321
295
  puts "Recorded start of work for #{issue.name}."
322
296
  end
323
297
 
324
- operation :stop, "Stop work on an issue", :issue
298
+ operation :stop, "Stop work on an issue", :started_issue
325
299
  def stop project, config, issue
326
300
  puts "Stopping work on issue #{issue.name}: #{issue.title}."
327
301
  comment = ask_multiline "Comments" unless $opts[:no_comment]
@@ -329,7 +303,7 @@ EOS
329
303
  puts "Recorded work stop for #{issue.name}."
330
304
  end
331
305
 
332
- operation :close, "Close an issue", :issue
306
+ operation :close, "Close an issue", :open_issue
333
307
  def close project, config, issue
334
308
  puts "Closing issue #{issue.name}: #{issue.title}."
335
309
  disp = ask_for_selection Issue::DISPOSITIONS, "disposition", lambda { |x| Issue::DISPOSITION_STRINGS[x] || x.to_s }
@@ -395,7 +369,7 @@ have changed as well.
395
369
  EOS
396
370
  end
397
371
 
398
- operation :unassign, "Unassign an issue from any releases", :issue
372
+ operation :unassign, "Unassign an issue from any releases", :assigned_issue
399
373
  def unassign project, config, issue
400
374
  puts "Unassigning issue #{issue.name}: #{issue.title}."
401
375
  comment = ask_multiline "Comments" unless $opts[:no_comment]
@@ -424,7 +398,7 @@ EOS
424
398
  end
425
399
  end
426
400
 
427
- operation :release, "Release a release", :release
401
+ operation :release, "Release a release", :unreleased_release
428
402
  def release project, config, release
429
403
  comment = ask_multiline "Comments" unless $opts[:no_comment]
430
404
  release.release! project, config.user, comment
@@ -448,69 +422,7 @@ EOS
448
422
  operation :html, "Generate html status pages", :maybe_dir
449
423
  def html project, config, dir
450
424
  dir ||= "html"
451
- Dir.mkdir dir unless File.exists? dir
452
-
453
- ## find the ERB templates. this is my brilliant approach
454
- ## to the 'gem datadir' problem.
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
458
-
459
- FileUtils.cp File.join(template_dir, "style.css"), dir
460
-
461
- ## build up links
462
- links = {}
463
- project.releases.each { |r| links[r] = "release-#{r.name}.html" }
464
- project.issues.each { |i| links[i] = "issue-#{i.id}.html" }
465
- project.components.each { |c| links[c] = "component-#{c.name}.html" }
466
- links["unassigned"] = "unassigned.html" # special case: unassigned
467
- links["index"] = "index.html" # special case: index
468
-
469
- project.issues.each do |issue|
470
- fn = File.join dir, links[issue]
471
- puts "Generating #{fn}..."
472
- File.open(fn, "w") do |f|
473
- f.puts ErbHtml.new(template_dir, "issue", links, :issue => issue,
474
- :release => (issue.release ? project.release_for(issue.release) : nil),
475
- :component => project.component_for(issue.component),
476
- :project => project)
477
- end
478
- end
479
-
480
- project.releases.each do |r|
481
- fn = File.join dir, links[r]
482
- puts "Generating #{fn}..."
483
- File.open(fn, "w") do |f|
484
- f.puts ErbHtml.new(template_dir, "release", links, :release => r,
485
- :issues => project.issues_for_release(r), :project => project)
486
- end
487
- end
488
-
489
- project.components.each do |c|
490
- fn = File.join dir, links[c]
491
- puts "Generating #{fn}..."
492
- File.open(fn, "w") do |f|
493
- f.puts ErbHtml.new(template_dir, "component", links, :component => c,
494
- :issues => project.issues_for_component(c), :project => project)
495
- end
496
- end
497
-
498
- fn = File.join dir, links["unassigned"]
499
- puts "Generating #{fn}..."
500
- File.open(fn, "w") do |f|
501
- f.puts ErbHtml.new(template_dir, "unassigned", links,
502
- :issues => project.unassigned_issues, :project => project)
503
- end
504
-
505
- past_rels, upcoming_rels = project.releases.partition { |r| r.released? }
506
- fn = File.join dir, links["index"]
507
- puts "Generating #{fn}..."
508
- File.open(fn, "w") do |f|
509
- f.puts ErbHtml.new(template_dir, "index", links, :project => project,
510
- :past_releases => past_rels, :upcoming_releases => upcoming_rels,
511
- :components => project.components)
512
- end
513
- puts "Local generated URL: file://#{File.expand_path(fn)}"
425
+ HtmlView.new(project, config, dir).render_all
514
426
  end
515
427
 
516
428
  operation :validate, "Validate project status"
@@ -521,7 +433,10 @@ EOS
521
433
  operation :grep, "Show issues matching a string or regular expression", :string
522
434
  def grep project, config, match
523
435
  re = /#{match}/
524
- issues = project.issues.select { |i| i.title =~ re || i.desc =~ re }
436
+ issues = project.issues.select do |i|
437
+ i.title =~ re || i.desc =~ re ||
438
+ i.log_events.map { |time, who, what, comments| comments }.join(" ") =~ re
439
+ end
525
440
  puts(todo_list_for(issues) || "No matching issues.")
526
441
  end
527
442
 
@@ -536,7 +451,7 @@ author: #{author}
536
451
  issue: [#{i.name}] #{i.title}
537
452
 
538
453
  #{what}
539
- #{comment.multiline " > ", false}
454
+ #{comment.gsub(/^/, " > ") unless comment =~ /^\A\s*\z/}
540
455
  EOS
541
456
  puts unless comment.blank?
542
457
  end