ditz-str 0.0.1
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/.document +5 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +24 -0
- data/LICENSE +674 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/README.txt +143 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/bin/ditz-str +189 -0
- data/bugs/issue-02615b8c3dd0382c92f350ce2158ecfe94d11ef8.yaml +22 -0
- data/bugs/issue-06a3bbf35a60c4da2d8ea0fdc86164263126d6b2.yaml +22 -0
- data/bugs/issue-0c00c1d7fdffaad304e62d79d9b3d5e92547055b.yaml +38 -0
- data/bugs/issue-20dad4b4533d6d76d496fe5970098f1eb8efd561.yaml +26 -0
- data/bugs/issue-360ae6529dbc66358fde6b532cbea79ece37a670.yaml +22 -0
- data/bugs/issue-5177d61bf3c2783f71ef63e6e2c5e720247ef699.yaml +18 -0
- data/bugs/issue-695b564c210da1965a2bb38eef782178aead6952.yaml +26 -0
- data/bugs/issue-7d0ce6429a9fb5fa09ce3376a8921a5ecb7ecfe5.yaml +34 -0
- data/bugs/issue-a04462fa22ab6e1b02cfdd052d1f6c6f491f08f5.yaml +22 -0
- data/bugs/issue-bca54ca5107eabc3b281701041cc36ea0641cbdd.yaml +26 -0
- data/bugs/issue-d0c7d04b014d705c5fd865e4d487b5e5b6983c33.yaml +26 -0
- data/bugs/issue-f94b879842aa0274aa74fc2833252d4a06ec65cc.yaml +22 -0
- data/bugs/project.yaml +18 -0
- data/data/ditz-str/blue-check.png +0 -0
- data/data/ditz-str/close.rhtml +39 -0
- data/data/ditz-str/component.rhtml +38 -0
- data/data/ditz-str/dropdown.css +11 -0
- data/data/ditz-str/dropdown.js +58 -0
- data/data/ditz-str/edit_issue.rhtml +53 -0
- data/data/ditz-str/green-bar.png +0 -0
- data/data/ditz-str/green-check.png +0 -0
- data/data/ditz-str/header.gif +0 -0
- data/data/ditz-str/header_over.gif +0 -0
- data/data/ditz-str/index.rhtml +148 -0
- data/data/ditz-str/issue.rhtml +152 -0
- data/data/ditz-str/issue_table.rhtml +28 -0
- data/data/ditz-str/new_component.rhtml +28 -0
- data/data/ditz-str/new_issue.rhtml +57 -0
- data/data/ditz-str/new_release.rhtml +29 -0
- data/data/ditz-str/plugins/git-sync.rb +83 -0
- data/data/ditz-str/plugins/git.rb +153 -0
- data/data/ditz-str/plugins/issue-claiming.rb +174 -0
- data/data/ditz-str/red-check.png +0 -0
- data/data/ditz-str/release.rhtml +111 -0
- data/data/ditz-str/style.css +236 -0
- data/data/ditz-str/unassigned.rhtml +37 -0
- data/data/ditz-str/yellow-bar.png +0 -0
- data/ditz-str.gemspec +121 -0
- data/lib/ditzstr/brick.rb +251 -0
- data/lib/ditzstr/file-storage.rb +54 -0
- data/lib/ditzstr/hook.rb +67 -0
- data/lib/ditzstr/html.rb +104 -0
- data/lib/ditzstr/lowline.rb +201 -0
- data/lib/ditzstr/model-objects.rb +346 -0
- data/lib/ditzstr/model.rb +265 -0
- data/lib/ditzstr/operator.rb +593 -0
- data/lib/ditzstr/trollop.rb +614 -0
- data/lib/ditzstr/util.rb +61 -0
- data/lib/ditzstr/view.rb +16 -0
- data/lib/ditzstr/views.rb +157 -0
- data/lib/ditzstr.rb +69 -0
- data/man/ditz.1 +38 -0
- data/test/helper.rb +18 -0
- data/test/test_ditz-str.rb +7 -0
- metadata +219 -0
@@ -0,0 +1,593 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module DitzStr
|
4
|
+
|
5
|
+
class Operator
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def method_to_op meth; meth.to_s.gsub("_", "-") end
|
10
|
+
def op_to_method op; op.gsub("-", "_").intern end
|
11
|
+
|
12
|
+
def operation method, desc, *args_spec, &options_blk
|
13
|
+
@operations ||= {}
|
14
|
+
@operations[method] = { :desc => desc, :args_spec => args_spec,
|
15
|
+
:options_blk => options_blk }
|
16
|
+
end
|
17
|
+
|
18
|
+
def operations
|
19
|
+
@operations.map { |k, v| [method_to_op(k), v] }.sort_by { |k, v| k }
|
20
|
+
end
|
21
|
+
def has_operation? op; @operations.member? op_to_method(op) end
|
22
|
+
|
23
|
+
def build_opts method, args
|
24
|
+
options_blk = @operations[method][:options_blk]
|
25
|
+
options_blk and options args, &options_blk or nil
|
26
|
+
end
|
27
|
+
|
28
|
+
## parse the specs, and the commandline arguments, and resolve them. does
|
29
|
+
## typechecking but currently doesn't check for open_issues actually being
|
30
|
+
## open, unstarted_issues being unstarted, etc. probably will check for
|
31
|
+
## this in the future.
|
32
|
+
def build_args project, method, args
|
33
|
+
specs = @operations[method][:args_spec]
|
34
|
+
command = "command '#{method_to_op method}'"
|
35
|
+
|
36
|
+
if specs.empty? && args == ["<options>"]
|
37
|
+
die_with_completions project, method, nil
|
38
|
+
end
|
39
|
+
|
40
|
+
built_args = specs.map do |spec|
|
41
|
+
optional = spec.to_s =~ /^maybe_/
|
42
|
+
spec = spec.to_s.gsub(/^maybe_/, "").intern # :(
|
43
|
+
val = args.shift
|
44
|
+
|
45
|
+
case val
|
46
|
+
when nil
|
47
|
+
next if optional
|
48
|
+
specname = spec.to_s.gsub("_", " ")
|
49
|
+
article = specname =~ /^[aeiou]/ ? "an" : "a"
|
50
|
+
raise Error, "#{command} requires #{article} #{specname}"
|
51
|
+
when "<options>"
|
52
|
+
die_with_completions project, method, spec
|
53
|
+
end
|
54
|
+
|
55
|
+
case spec
|
56
|
+
when :issue, :open_issue, :unstarted_issue, :started_issue, :assigned_issue
|
57
|
+
## issue completion sticks the title on there, so this will strip it off
|
58
|
+
valr = val.sub(/\A(\w+-\d+)_.*$/,'\1')
|
59
|
+
issues = project.issues_for valr
|
60
|
+
case issues.size
|
61
|
+
when 0; raise Error, "no issue with name #{val.inspect}"
|
62
|
+
when 1; issues.first
|
63
|
+
else
|
64
|
+
raise Error, "multiple issues matching name #{val.inspect}"
|
65
|
+
end
|
66
|
+
when :release, :unreleased_release
|
67
|
+
if val == "unassigned"
|
68
|
+
:unassigned
|
69
|
+
else
|
70
|
+
project.release_for(val) or raise Error, "no release with name #{val}"
|
71
|
+
end
|
72
|
+
when :component
|
73
|
+
project.component_for(val) or raise Error, "no component with name #{val}" if val
|
74
|
+
else
|
75
|
+
val # no translation for other types
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
raise Error, "too many arguments for #{command}" unless args.empty?
|
80
|
+
built_args
|
81
|
+
end
|
82
|
+
|
83
|
+
def die_with_completions project, method, spec
|
84
|
+
puts(case spec
|
85
|
+
when :issue, :open_issue, :unstarted_issue, :started_issue, :assigned_issue
|
86
|
+
m = { :issue => nil,
|
87
|
+
:open_issue => :open?,
|
88
|
+
:unstarted_issue => :unstarted?,
|
89
|
+
:started_issue => :in_progress?,
|
90
|
+
:assigned_issue => :assigned?,
|
91
|
+
}[spec]
|
92
|
+
project.issues.select { |i| m.nil? || i.send(m) }.sort_by { |i| i.creation_time }.reverse.map { |i| "#{i.name}_#{i.title.gsub(/\W+/, '-')}" }
|
93
|
+
when :release
|
94
|
+
project.releases.map { |r| r.name } + ["unassigned"]
|
95
|
+
when :unreleased_release
|
96
|
+
project.releases.select { |r| r.unreleased? }.map { |r| r.name }
|
97
|
+
when :component
|
98
|
+
project.components.map { |c| c.name }
|
99
|
+
when :command
|
100
|
+
operations.map { |name, _| name }
|
101
|
+
else
|
102
|
+
""
|
103
|
+
end)
|
104
|
+
exit 0
|
105
|
+
end
|
106
|
+
private :die_with_completions
|
107
|
+
end
|
108
|
+
|
109
|
+
def do op, project, config, args
|
110
|
+
meth = self.class.op_to_method(op)
|
111
|
+
|
112
|
+
# Parse options, removing them from args
|
113
|
+
opts = self.class.build_opts meth, args
|
114
|
+
built_args = self.class.build_args project, meth, args
|
115
|
+
|
116
|
+
built_args.unshift opts if opts
|
117
|
+
|
118
|
+
send meth, project, config, *built_args
|
119
|
+
end
|
120
|
+
|
121
|
+
%w(operations has_operation?).each do |m|
|
122
|
+
define_method(m) { |*a| self.class.send m, *a }
|
123
|
+
end
|
124
|
+
|
125
|
+
operation :init, "Initialize the issue database for a new project"
|
126
|
+
def init
|
127
|
+
Project.create_interactively
|
128
|
+
end
|
129
|
+
|
130
|
+
operation :help, "List all registered commands", :maybe_command do
|
131
|
+
opt :cow, "Activate super cow powers", :default => false
|
132
|
+
end
|
133
|
+
def help project, config, opts, command
|
134
|
+
if opts[:cow]
|
135
|
+
puts "MOO!"
|
136
|
+
puts "All is well with the world now. A bit more methane though."
|
137
|
+
return
|
138
|
+
end
|
139
|
+
return help_single(command) if command
|
140
|
+
puts <<EOS
|
141
|
+
DitzStr commands:
|
142
|
+
|
143
|
+
EOS
|
144
|
+
ops = self.class.operations
|
145
|
+
len = ops.map { |name, op| name.to_s.length }.max
|
146
|
+
ops.each do |name, opts|
|
147
|
+
printf " %#{len}s: %s\n", name, opts[:desc]
|
148
|
+
end
|
149
|
+
puts <<EOS
|
150
|
+
|
151
|
+
Use 'ditz help <command>' for details.
|
152
|
+
EOS
|
153
|
+
end
|
154
|
+
|
155
|
+
def help_single command
|
156
|
+
name, opts = self.class.operations.find { |name, spec| name == command }
|
157
|
+
raise Error, "no such ditz command '#{command}'" unless name
|
158
|
+
args = opts[:args_spec].map do |spec|
|
159
|
+
case spec.to_s
|
160
|
+
when /^maybe_(.*)$/
|
161
|
+
"[#{$1}]"
|
162
|
+
else
|
163
|
+
"<#{spec.to_s}>"
|
164
|
+
end
|
165
|
+
end.join(" ")
|
166
|
+
|
167
|
+
puts <<EOS
|
168
|
+
#{opts[:desc]}.
|
169
|
+
Usage: ditz #{name} #{args}
|
170
|
+
EOS
|
171
|
+
end
|
172
|
+
|
173
|
+
## gets a comment from the user, assuming the standard argument setup
|
174
|
+
def get_comment opts
|
175
|
+
comment = if opts[:no_comment]
|
176
|
+
nil
|
177
|
+
elsif opts[:comment]
|
178
|
+
opts[:comment]
|
179
|
+
else
|
180
|
+
ask_multiline "Comments"
|
181
|
+
end
|
182
|
+
end
|
183
|
+
private :get_comment
|
184
|
+
|
185
|
+
operation :reconfigure, "Rerun configuration script"
|
186
|
+
def reconfigure project, config
|
187
|
+
new_config = Config.create_interactively :defaults_from => config
|
188
|
+
new_config.save! $opts[:config_file]
|
189
|
+
puts "Configuration written."
|
190
|
+
end
|
191
|
+
|
192
|
+
operation :add, "Add an issue" do
|
193
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
194
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
195
|
+
end
|
196
|
+
def add project, config, opts
|
197
|
+
issue = Issue.create_interactively(:args => [config, project]) or return
|
198
|
+
issue.log "created", config.user, get_comment(opts)
|
199
|
+
project.add_issue issue
|
200
|
+
project.assign_issue_names!
|
201
|
+
puts "Added issue #{issue.name}."
|
202
|
+
end
|
203
|
+
|
204
|
+
operation :drop, "Drop an issue", :issue
|
205
|
+
def drop project, config, issue
|
206
|
+
project.drop_issue issue
|
207
|
+
puts "Dropped #{issue.name}. Note that other issue names may have changed."
|
208
|
+
end
|
209
|
+
|
210
|
+
operation :add_release, "Add a release", :maybe_name do
|
211
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
212
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
213
|
+
end
|
214
|
+
def add_release project, config, opts, maybe_name
|
215
|
+
puts "Adding release #{maybe_name}." if maybe_name
|
216
|
+
release = Release.create_interactively(:args => [project, config], :with => { :name => maybe_name }) or return
|
217
|
+
release.log "created", config.user, get_comment(opts)
|
218
|
+
project.add_release release
|
219
|
+
puts "Added release #{release.name}."
|
220
|
+
end
|
221
|
+
|
222
|
+
operation :add_component, "Add a component"
|
223
|
+
def add_component project, config
|
224
|
+
component = Component.create_interactively(:args => [project, config]) or return
|
225
|
+
project.add_component component
|
226
|
+
puts "Added component #{component.name}."
|
227
|
+
end
|
228
|
+
|
229
|
+
operation :add_reference, "Add a reference to an issue", :issue do
|
230
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
231
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
232
|
+
end
|
233
|
+
def add_reference project, config, opts, issue
|
234
|
+
puts "Adding a reference to #{issue.name}: #{issue.title}."
|
235
|
+
reference = ask "Reference"
|
236
|
+
issue.add_reference reference
|
237
|
+
issue.log "added reference #{issue.references.size}", config.user, get_comment(opts)
|
238
|
+
puts "Added reference to #{issue.name}."
|
239
|
+
end
|
240
|
+
|
241
|
+
operation :status, "Show project status", :maybe_release
|
242
|
+
def status project, config, releases
|
243
|
+
releases ||= project.unreleased_releases + [:unassigned]
|
244
|
+
|
245
|
+
if releases.empty?
|
246
|
+
puts "No releases."
|
247
|
+
return
|
248
|
+
end
|
249
|
+
|
250
|
+
entries = releases.map do |r|
|
251
|
+
title, issues = (r == :unassigned ? r.to_s : r.name), project.issues_for_release(r)
|
252
|
+
|
253
|
+
middle = Issue::TYPES.map do |type|
|
254
|
+
type_issues = issues.select { |i| i.type == type }
|
255
|
+
num = type_issues.size
|
256
|
+
nc = type_issues.count_of { |i| i.closed? }
|
257
|
+
pc = 100.0 * (type_issues.empty? ? 1.0 : nc.to_f / num)
|
258
|
+
"%2d/%2d %s" % [nc, num, type.to_s.pluralize(num, false)]
|
259
|
+
end
|
260
|
+
|
261
|
+
bar = if r == :unassigned
|
262
|
+
""
|
263
|
+
elsif r.released?
|
264
|
+
"(released)"
|
265
|
+
elsif issues.empty?
|
266
|
+
"(no issues)"
|
267
|
+
elsif issues.all? { |i| i.closed? }
|
268
|
+
"(ready for release)"
|
269
|
+
else
|
270
|
+
status_bar_for(issues)
|
271
|
+
end
|
272
|
+
|
273
|
+
[title, middle, bar]
|
274
|
+
end
|
275
|
+
|
276
|
+
title_size = 0
|
277
|
+
middle_sizes = []
|
278
|
+
|
279
|
+
entries.each do |title, middle, bar|
|
280
|
+
title_size = [title_size, title.length].max
|
281
|
+
middle_sizes = middle.zip(middle_sizes).map do |e, s|
|
282
|
+
[s || 0, e.length].max
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
entries.each do |title, middle, bar|
|
287
|
+
printf "%-#{title_size}s ", title
|
288
|
+
middle.zip(middle_sizes).each_with_index do |(e, s), i|
|
289
|
+
sep = i < middle.size - 1 ? "," : ""
|
290
|
+
printf "%-#{s + sep.length}s ", e + sep
|
291
|
+
end
|
292
|
+
puts bar
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
def status_bar_for issues
|
297
|
+
Issue::STATUS_WIDGET.
|
298
|
+
sort_by { |k, v| -Issue::STATUS_SORT_ORDER[k] }.
|
299
|
+
map { |k, v| v * issues.count_of { |i| i.status == k } }.
|
300
|
+
join
|
301
|
+
end
|
302
|
+
|
303
|
+
def todo_list_for issues, opts={}
|
304
|
+
return if issues.empty?
|
305
|
+
name_len = issues.max_of { |i| i.name.length }
|
306
|
+
issues.map do |i|
|
307
|
+
s = sprintf "%s %#{name_len}s: %s", i.status_widget, i.name, i.title
|
308
|
+
s += " [#{i.release}]" if opts[:show_release] && i.release
|
309
|
+
s + "\n"
|
310
|
+
end.join
|
311
|
+
end
|
312
|
+
|
313
|
+
def print_todo_list_by_release_for project, issues
|
314
|
+
by_release = issues.inject({}) do |h, i|
|
315
|
+
r = project.release_for(i.release) || :unassigned
|
316
|
+
h[r] ||= []
|
317
|
+
h[r] << i
|
318
|
+
h
|
319
|
+
end
|
320
|
+
|
321
|
+
(project.releases + [:unassigned]).each do |r|
|
322
|
+
next unless by_release.member? r
|
323
|
+
puts r == :unassigned ? "Unassigned:" : "#{r.name} (#{r.status}):"
|
324
|
+
print todo_list_for(by_release[r])
|
325
|
+
puts
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
operation :todo, "Generate todo list", :maybe_release do
|
330
|
+
opt :all, "Show all issues, included completed ones", :default => false
|
331
|
+
end
|
332
|
+
def todo project, config, opts, releases
|
333
|
+
actually_do_todo project, config, releases, opts[:all]
|
334
|
+
end
|
335
|
+
|
336
|
+
def actually_do_todo project, config, releases, full
|
337
|
+
releases ||= project.unreleased_releases + [:unassigned]
|
338
|
+
releases = [*releases]
|
339
|
+
releases.each do |r|
|
340
|
+
puts r == :unassigned ? "Unassigned:" : "#{r.name} (#{r.status}):"
|
341
|
+
issues = project.issues_for_release r
|
342
|
+
issues = issues.select { |i| i.open? } unless full
|
343
|
+
puts(todo_list_for(issues.sort_by { |i| i.sort_order }) || "No open issues.")
|
344
|
+
puts
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
operation :show, "Describe a single issue", :issue
|
349
|
+
def show project, config, issue
|
350
|
+
ScreenView.new(project, config).render_issue issue
|
351
|
+
end
|
352
|
+
|
353
|
+
operation :start, "Start work on an issue", :unstarted_issue do
|
354
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
355
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
356
|
+
end
|
357
|
+
def start project, config, opts, issue
|
358
|
+
puts "Starting work on issue #{issue.name}: #{issue.title}."
|
359
|
+
issue.start_work config.user, get_comment(opts)
|
360
|
+
puts "Recorded start of work for #{issue.name}."
|
361
|
+
end
|
362
|
+
|
363
|
+
operation :stop, "Stop work on an issue", :started_issue do
|
364
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
365
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
366
|
+
end
|
367
|
+
def stop project, config, opts, issue
|
368
|
+
puts "Stopping work on issue #{issue.name}: #{issue.title}."
|
369
|
+
issue.stop_work config.user, get_comment(opts)
|
370
|
+
puts "Recorded work stop for #{issue.name}."
|
371
|
+
end
|
372
|
+
|
373
|
+
operation :close, "Close an issue", :open_issue do
|
374
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
375
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
376
|
+
end
|
377
|
+
def close project, config, opts, issue
|
378
|
+
puts "Closing issue #{issue.name}: #{issue.title}."
|
379
|
+
disp = ask_for_selection Issue::DISPOSITIONS, "disposition", lambda { |x| Issue::DISPOSITION_STRINGS[x] || x.to_s }
|
380
|
+
issue.close disp, config.user, get_comment(opts)
|
381
|
+
puts "Closed issue #{issue.name} with disposition #{issue.disposition_string}."
|
382
|
+
end
|
383
|
+
|
384
|
+
operation :assign, "Assign an issue to a release", :issue, :maybe_release do
|
385
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
386
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
387
|
+
end
|
388
|
+
def assign project, config, opts, issue, maybe_release
|
389
|
+
if maybe_release && maybe_release.name == issue.release
|
390
|
+
raise Error, "issue #{issue.name} already assigned to release #{issue.release}"
|
391
|
+
end
|
392
|
+
|
393
|
+
puts "Issue #{issue.name} currently " + if issue.release
|
394
|
+
"assigned to release #{issue.release}."
|
395
|
+
else
|
396
|
+
"not assigned to any release."
|
397
|
+
end
|
398
|
+
|
399
|
+
release = maybe_release || begin
|
400
|
+
releases = project.releases.sort_by { |r| (r.release_time || 0).to_i }
|
401
|
+
releases -= [releases.find { |r| r.name == issue.release }] if issue.release
|
402
|
+
ask_for_selection(releases, "release") do |r|
|
403
|
+
r.name + if r.released?
|
404
|
+
" (released #{r.release_time.pretty_date})"
|
405
|
+
else
|
406
|
+
" (unreleased)"
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
issue.assign_to_release release, config.user, get_comment(opts)
|
411
|
+
puts "Assigned #{issue.name} to #{release.name}."
|
412
|
+
end
|
413
|
+
|
414
|
+
operation :set_component, "Set an issue's component", :issue, :maybe_component do
|
415
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
416
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
417
|
+
end
|
418
|
+
def set_component project, config, opts, issue, maybe_component
|
419
|
+
puts "Changing the component of issue #{issue.name}: #{issue.title}."
|
420
|
+
|
421
|
+
if project.components.size == 1
|
422
|
+
raise Error, "this project does not use multiple components"
|
423
|
+
end
|
424
|
+
|
425
|
+
if maybe_component && maybe_component.name == issue.component
|
426
|
+
raise Error, "issue #{issue.name} already assigned to component #{issue.component}"
|
427
|
+
end
|
428
|
+
|
429
|
+
component = maybe_component || begin
|
430
|
+
components = project.components
|
431
|
+
components -= [components.find { |r| r.name == issue.component }] if issue.component
|
432
|
+
ask_for_selection(components, "component") { |r| r.name }
|
433
|
+
end
|
434
|
+
issue.assign_to_component component, config.user, get_comment(opts)
|
435
|
+
oldname = issue.name
|
436
|
+
project.assign_issue_names!
|
437
|
+
puts <<EOS
|
438
|
+
Issue #{oldname} is now #{issue.name}. Note that the names of other issues may
|
439
|
+
have changed as well.
|
440
|
+
EOS
|
441
|
+
end
|
442
|
+
|
443
|
+
operation :unassign, "Unassign an issue from any releases", :assigned_issue do
|
444
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
445
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
446
|
+
end
|
447
|
+
def unassign project, config, opts, issue
|
448
|
+
puts "Unassigning issue #{issue.name}: #{issue.title}."
|
449
|
+
issue.unassign config.user, get_comment(opts)
|
450
|
+
puts "Unassigned #{issue.name}."
|
451
|
+
end
|
452
|
+
|
453
|
+
operation :comment, "Comment on an issue", :issue do
|
454
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
455
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
456
|
+
end
|
457
|
+
def comment project, config, opts, issue
|
458
|
+
puts "Commenting on issue #{issue.name}: #{issue.title}."
|
459
|
+
comment = get_comment opts
|
460
|
+
if comment.blank?
|
461
|
+
puts "Empty comment, aborted."
|
462
|
+
else
|
463
|
+
issue.log "commented", config.user, comment
|
464
|
+
puts "Comments recorded for #{issue.name}."
|
465
|
+
end
|
466
|
+
end
|
467
|
+
|
468
|
+
operation :releases, "Show releases"
|
469
|
+
def releases project, config
|
470
|
+
a, b = project.releases.partition { |r| r.released? }
|
471
|
+
(b + a.sort_by { |r| r.release_time }).each do |r|
|
472
|
+
status = r.released? ? "released #{r.release_time.pretty_date}" : r.status
|
473
|
+
puts "#{r.name} (#{status})"
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
operation :release, "Release a release", :unreleased_release do
|
478
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
479
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
480
|
+
end
|
481
|
+
def release project, config, opts, release
|
482
|
+
release.release! project, config.user, get_comment(opts)
|
483
|
+
puts "Release #{release.name} released!"
|
484
|
+
end
|
485
|
+
|
486
|
+
operation :changelog, "Generate a changelog for a release", :release
|
487
|
+
def changelog project, config, r
|
488
|
+
puts "== #{r.name} / #{r.released? ? r.release_time.pretty_date : 'unreleased'}"
|
489
|
+
project.group_issues(project.issues_for_release(r)).each do |type, issues|
|
490
|
+
issues.select { |i| i.closed? }.each do |i|
|
491
|
+
if type == :bugfix
|
492
|
+
puts "* #{type}: #{i.title}"
|
493
|
+
else
|
494
|
+
puts "* #{i.title}"
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end
|
499
|
+
|
500
|
+
operation :html, "Generate html status pages", :maybe_dir
|
501
|
+
def html project, config, dir
|
502
|
+
dir ||= "html"
|
503
|
+
HtmlView.new(project, config, dir).render_all
|
504
|
+
end
|
505
|
+
|
506
|
+
operation :validate, "Validate project status"
|
507
|
+
def validate project, config
|
508
|
+
## a no-op
|
509
|
+
end
|
510
|
+
|
511
|
+
operation :grep, "Show issues matching a string or regular expression", :string
|
512
|
+
def grep project, config, match
|
513
|
+
re = /#{match}/
|
514
|
+
issues = project.issues.select do |i|
|
515
|
+
i.title =~ re || i.desc =~ re ||
|
516
|
+
i.log_events.map { |time, who, what, comments| comments }.join(" ") =~ re
|
517
|
+
end
|
518
|
+
puts(todo_list_for(issues) || "No matching issues.")
|
519
|
+
end
|
520
|
+
|
521
|
+
operation :log, "Show recent activity"
|
522
|
+
def log project, config
|
523
|
+
project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
|
524
|
+
flatten_one_level.sort_by { |e| e.first.first }.reverse.
|
525
|
+
each do |(date, author, what, comment), i|
|
526
|
+
puts <<EOS
|
527
|
+
date : #{date.localtime} (#{date.ago} ago)
|
528
|
+
author: #{author}
|
529
|
+
issue: [#{i.name}] #{i.title}
|
530
|
+
|
531
|
+
#{what}
|
532
|
+
#{comment.gsub(/^/, " > ") unless comment =~ /^\A\s*\z/}
|
533
|
+
EOS
|
534
|
+
puts unless comment.blank?
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
operation :shortlog, "Show recent activity (short form)"
|
539
|
+
def shortlog project, config
|
540
|
+
project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
|
541
|
+
flatten_one_level.sort_by { |e| e.first.first }.reverse.
|
542
|
+
each do |(date, author, what, comment), i|
|
543
|
+
shortauthor = if author =~ /<(.*?)@/
|
544
|
+
$1
|
545
|
+
else
|
546
|
+
author
|
547
|
+
end[0..15]
|
548
|
+
printf "%13s|%13s|%13s|%s\n", date.ago, i.name, shortauthor,
|
549
|
+
what
|
550
|
+
end
|
551
|
+
end
|
552
|
+
|
553
|
+
operation :archive, "Archive a release", :release, :maybe_dir
|
554
|
+
def archive project, config, release, dir
|
555
|
+
dir ||= "ditz-archive-#{release.name}"
|
556
|
+
FileUtils.mkdir dir
|
557
|
+
FileUtils.cp project.pathname, dir
|
558
|
+
project.issues_for_release(release).each do |i|
|
559
|
+
FileUtils.cp i.pathname, dir
|
560
|
+
project.drop_issue i
|
561
|
+
end
|
562
|
+
puts "Archived to #{dir}."
|
563
|
+
end
|
564
|
+
|
565
|
+
operation :edit, "Edit an issue", :issue do
|
566
|
+
opt :comment, "Specify a comment", :short => 'm', :type => String
|
567
|
+
opt :no_comment, "Skip asking for a comment", :default => false
|
568
|
+
opt :silent, "Don't add a log message detailing the change", :default => false
|
569
|
+
end
|
570
|
+
def edit project, config, opts, issue
|
571
|
+
data = { :title => issue.title, :description => issue.desc,
|
572
|
+
:reporter => issue.reporter }
|
573
|
+
|
574
|
+
fn = run_editor { |f| f.puts data.to_yaml }
|
575
|
+
|
576
|
+
unless fn
|
577
|
+
puts "Aborted."
|
578
|
+
return
|
579
|
+
end
|
580
|
+
|
581
|
+
begin
|
582
|
+
edits = YAML.load_file fn
|
583
|
+
comment = opts[:silent] ? nil : get_comment(opts)
|
584
|
+
if issue.change edits, config.user, comment, opts[:silent]
|
585
|
+
puts "Change recorded."
|
586
|
+
else
|
587
|
+
puts "No changes."
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
end
|