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