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.
- data/Changelog +9 -0
- data/README.txt +22 -11
- data/Rakefile +1 -15
- data/ReleaseNotes +29 -0
- data/bin/ditz +32 -20
- data/contrib/completion/_ditz.zsh +29 -0
- data/contrib/completion/ditz.bash +22 -0
- data/lib/ditz.rb +29 -2
- data/lib/hook.rb +9 -2
- data/lib/html.rb +36 -14
- data/lib/index.rhtml +24 -1
- data/lib/issue.rhtml +6 -1
- data/lib/issue_table.rhtml +6 -2
- data/lib/lowline.rb +1 -8
- data/lib/model-objects.rb +7 -2
- data/lib/model.rb +12 -4
- data/lib/operator.rb +81 -166
- data/lib/plugins/git.rb +101 -0
- data/lib/style.css +39 -3
- data/lib/unassigned.rhtml +1 -1
- data/lib/util.rb +5 -0
- data/lib/view.rb +16 -0
- data/lib/views.rb +136 -0
- metadata +22 -8
- data/bin/ditz-convert-from-monolith +0 -42
data/lib/issue.rhtml
CHANGED
@@ -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"><%=
|
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>
|
data/lib/issue_table.rhtml
CHANGED
@@ -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
|
-
|
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
|
-
<%=
|
12
|
+
<%= fancy_issue_link_for i %>
|
9
13
|
<%= i.bug? ? '(bug)' : '' %>
|
10
14
|
</td>
|
11
15
|
<% if show_release %>
|
data/lib/lowline.rb
CHANGED
@@ -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
|
|
data/lib/model-objects.rb
CHANGED
@@ -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
|
205
|
+
log "closed with disposition #{disp}", who, comment
|
201
206
|
self.status = :closed
|
202
207
|
self.disposition = disp
|
203
208
|
end
|
data/lib/model.rb
CHANGED
@@ -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
|
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
|
-
|
141
|
-
|
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
|
-
|
162
|
+
end
|
155
163
|
|
156
164
|
def log what, who, comment
|
157
165
|
add_log_event([Time.now, who, what, comment || ""])
|
data/lib/operator.rb
CHANGED
@@ -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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
36
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
66
|
+
|
76
67
|
raise Error, "too many arguments for #{command}" unless args.empty?
|
77
68
|
built_args
|
78
69
|
end
|
79
70
|
|
80
|
-
def
|
81
|
-
case spec
|
82
|
-
when :issue
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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", :
|
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 =
|
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", :
|
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", :
|
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.
|
272
|
-
|
273
|
-
|
274
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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", :
|
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", :
|
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", :
|
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", :
|
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
|
-
|
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
|
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.
|
454
|
+
#{comment.gsub(/^/, " > ") unless comment =~ /^\A\s*\z/}
|
540
455
|
EOS
|
541
456
|
puts unless comment.blank?
|
542
457
|
end
|