ditz 0.3 → 0.4

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