ditz 0.4 → 0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog +15 -0
- data/INSTALL +20 -0
- data/LICENSE +674 -0
- data/Manifest.txt +42 -0
- data/PLUGINS.txt +99 -0
- data/README.txt +50 -35
- data/Rakefile +34 -2
- data/ReleaseNotes +6 -0
- data/bin/ditz +45 -68
- data/contrib/completion/ditz.bash +7 -7
- data/lib/blue-check.png +0 -0
- data/lib/component.rhtml +10 -4
- data/lib/ditz.rb +15 -3
- data/lib/file-storage.rb +54 -0
- data/lib/green-bar.png +0 -0
- data/lib/green-check.png +0 -0
- data/lib/hook.rb +1 -1
- data/lib/html.rb +39 -4
- data/lib/index.rhtml +80 -59
- data/lib/issue.rhtml +91 -79
- data/lib/issue_table.rhtml +24 -29
- data/lib/lowline.rb +5 -6
- data/lib/model-objects.rb +50 -17
- data/lib/model.rb +84 -26
- data/lib/operator.rb +151 -70
- data/lib/plugins/git-sync.rb +83 -0
- data/lib/plugins/git.rb +67 -15
- data/lib/plugins/issue-claiming.rb +174 -0
- data/lib/red-check.png +0 -0
- data/lib/release.rhtml +69 -38
- data/lib/style.css +138 -39
- data/lib/trollop.rb +614 -0
- data/lib/unassigned.rhtml +11 -15
- data/lib/util.rb +4 -0
- data/lib/views.rb +3 -1
- data/lib/yellow-bar.png +0 -0
- data/man/ditz.1 +38 -0
- data/setup.rb +1585 -0
- data/www/index.html +152 -0
- data/www/main.css +45 -0
- metadata +26 -7
data/lib/issue_table.rhtml
CHANGED
@@ -1,33 +1,28 @@
|
|
1
1
|
<table>
|
2
|
-
|
3
|
-
|
4
|
-
<
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
<%= i
|
9
|
-
<% end %>
|
10
|
-
</td>
|
11
|
-
<td class="issuename">
|
12
|
-
<%= fancy_issue_link_for i %>
|
13
|
-
<%= i.bug? ? '(bug)' : '' %>
|
14
|
-
</td>
|
15
|
-
<% if show_release %>
|
16
|
-
<td class="issuerelease">
|
17
|
-
<% if i.release %>
|
18
|
-
<% r = project.release_for i.release %>
|
19
|
-
in <%= link_to r, "release #{i.release}" %>
|
20
|
-
(<%= r.status %>)
|
21
|
-
<% else %>
|
22
|
-
<% end %>
|
23
|
-
</td>
|
24
|
-
<% end %>
|
25
|
-
<% if show_component %>
|
26
|
-
<td class="issuecomponent">
|
27
|
-
component <%= link_to project.component_for(i.component), i.component %>
|
2
|
+
<tbody>
|
3
|
+
<% issues.sort_by { |i| i.creation_time }.reverse.each do |i| %>
|
4
|
+
<tr>
|
5
|
+
<td> <%= issue_status_img_for i %> </td>
|
6
|
+
<td class="littledate"><%= i.creation_time.pretty_date %></td>
|
7
|
+
<td class="issuename">
|
8
|
+
<%= issue_link_for i %>
|
28
9
|
</td>
|
29
|
-
|
30
|
-
|
31
|
-
<%
|
10
|
+
<% if show_release %>
|
11
|
+
<td class="issuerelease">
|
12
|
+
<% if i.release %>
|
13
|
+
<% r = project.release_for i.release %>
|
14
|
+
<%= link_to r, i.release %>
|
15
|
+
<% else %>
|
16
|
+
<% end %>
|
17
|
+
</td>
|
18
|
+
<% end %>
|
19
|
+
<% if show_component %>
|
20
|
+
<td class="issuecomponent">
|
21
|
+
component <%= link_to project.component_for(i.component), i.component %>
|
22
|
+
</td>
|
23
|
+
<% end %>
|
24
|
+
</tr>
|
25
|
+
<% end %>
|
26
|
+
</tbody>
|
32
27
|
</table>
|
33
28
|
|
data/lib/lowline.rb
CHANGED
@@ -85,7 +85,7 @@ module Lowline
|
|
85
85
|
ans = Readline::readline(prompt)
|
86
86
|
else
|
87
87
|
print prompt
|
88
|
-
ans = gets.strip
|
88
|
+
ans = STDIN.gets.strip
|
89
89
|
end
|
90
90
|
if opts[:default]
|
91
91
|
ans = opts[:default] if ans.blank?
|
@@ -98,11 +98,10 @@ module Lowline
|
|
98
98
|
|
99
99
|
def ask_via_editor q, default=nil
|
100
100
|
fn = run_editor do |f|
|
101
|
+
f.puts(default || "")
|
101
102
|
f.puts q.gsub(/^/, "## ")
|
102
103
|
f.puts "##"
|
103
|
-
f.puts "## Enter your text
|
104
|
-
f.puts
|
105
|
-
f.puts default if default
|
104
|
+
f.puts "## Enter your text above. Lines starting with a '#' will be ignored."
|
106
105
|
end
|
107
106
|
return unless fn
|
108
107
|
IO.read(fn).gsub(/^#.*$/, "").multistrip
|
@@ -115,7 +114,7 @@ module Lowline
|
|
115
114
|
if Ditz::has_readline?
|
116
115
|
line = Readline::readline('> ')
|
117
116
|
else
|
118
|
-
(line = gets) && line.strip!
|
117
|
+
(line = STDIN.gets) && line.strip!
|
119
118
|
end
|
120
119
|
if line
|
121
120
|
if Ditz::has_readline?
|
@@ -142,7 +141,7 @@ module Lowline
|
|
142
141
|
def ask_yon q
|
143
142
|
while true
|
144
143
|
print "#{q} (y/n): "
|
145
|
-
a = gets.strip
|
144
|
+
a = STDIN.gets.strip
|
146
145
|
break a if a =~ /^[yn]$/i
|
147
146
|
end =~ /y/i
|
148
147
|
end
|
data/lib/model-objects.rb
CHANGED
@@ -36,17 +36,37 @@ end
|
|
36
36
|
class Project < ModelObject
|
37
37
|
class Error < StandardError; end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
field :name, :default_generator => lambda { File.basename(Dir.pwd) }
|
39
|
+
field :name, :prompt => "Project name", :default_generator => lambda { File.basename(Dir.pwd) }
|
42
40
|
field :version, :default => Ditz::VERSION, :ask => false
|
43
41
|
field :components, :multi => true, :generator => :get_components
|
44
42
|
field :releases, :multi => true, :ask => false
|
45
43
|
|
44
|
+
attr_accessor :pathname
|
45
|
+
|
46
46
|
## issues are not model fields proper, so we build up their interface here.
|
47
|
-
|
48
|
-
def
|
49
|
-
|
47
|
+
attr_reader :issues
|
48
|
+
def issues= issues
|
49
|
+
@issues = issues
|
50
|
+
@issues.each { |i| i.project = self }
|
51
|
+
assign_issue_names!
|
52
|
+
issues
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_issue issue
|
56
|
+
added_issues << issue
|
57
|
+
issues << issue
|
58
|
+
issue.project = self
|
59
|
+
assign_issue_names!
|
60
|
+
issue
|
61
|
+
end
|
62
|
+
|
63
|
+
def drop_issue issue
|
64
|
+
if issues.delete issue
|
65
|
+
deleted_issues << issue
|
66
|
+
assign_issue_names!
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
50
70
|
def added_issues; @added_issues ||= [] end
|
51
71
|
def deleted_issues; @deleted_issues ||= [] end
|
52
72
|
|
@@ -61,8 +81,9 @@ EOS
|
|
61
81
|
([name] + comp_names).uniq.map { |n| Component.create_interactively :with => { :name => n } }
|
62
82
|
end
|
63
83
|
|
64
|
-
def
|
65
|
-
issues.find { |i| i.name ==
|
84
|
+
def issues_for ident
|
85
|
+
by_name = issues.find { |i| i.name == ident }
|
86
|
+
by_name ? [by_name] : issues.select { |i| i.id =~ /^#{Regexp::escape ident}/ }
|
66
87
|
end
|
67
88
|
|
68
89
|
def component_for component_name
|
@@ -106,6 +127,12 @@ EOS
|
|
106
127
|
raise Error, "more than one release named #{dup.inspect}"
|
107
128
|
end
|
108
129
|
end
|
130
|
+
|
131
|
+
def self.from *a
|
132
|
+
p = super(*a)
|
133
|
+
p.validate!
|
134
|
+
p
|
135
|
+
end
|
109
136
|
end
|
110
137
|
|
111
138
|
class Issue < ModelObject
|
@@ -193,6 +220,7 @@ class Issue < ModelObject
|
|
193
220
|
def feature?; type == :feature end
|
194
221
|
def unassigned?; release.nil? end
|
195
222
|
def assigned?; !unassigned? end
|
223
|
+
def paused?; status == :paused end
|
196
224
|
|
197
225
|
def start_work who, comment; change_status :in_progress, who, comment end
|
198
226
|
def stop_work who, comment
|
@@ -215,27 +243,29 @@ class Issue < ModelObject
|
|
215
243
|
end
|
216
244
|
private :change_status
|
217
245
|
|
218
|
-
def change hash, who, comment
|
246
|
+
def change hash, who, comment, silent
|
219
247
|
what = []
|
220
248
|
if title != hash[:title]
|
221
|
-
what << "
|
249
|
+
what << "title"
|
222
250
|
self.title = hash[:title]
|
223
251
|
end
|
224
252
|
|
225
253
|
if desc != hash[:description]
|
226
|
-
what << "
|
254
|
+
what << "description"
|
227
255
|
self.desc = hash[:description]
|
228
256
|
end
|
229
257
|
|
230
258
|
if reporter != hash[:reporter]
|
231
|
-
what << "
|
259
|
+
what << "reporter"
|
232
260
|
self.reporter = hash[:reporter]
|
233
261
|
end
|
234
262
|
|
235
|
-
unless what.empty?
|
236
|
-
log what.join(", "), who, comment
|
263
|
+
unless what.empty? || silent
|
264
|
+
log "edited " + what.join(", "), who, comment
|
237
265
|
true
|
238
266
|
end
|
267
|
+
|
268
|
+
!what.empty?
|
239
269
|
end
|
240
270
|
|
241
271
|
def assign_to_release release, who, comment
|
@@ -288,15 +318,18 @@ end
|
|
288
318
|
class Config < ModelObject
|
289
319
|
field :name, :prompt => "Your name", :default_generator => :get_default_name
|
290
320
|
field :email, :prompt => "Your email address", :default_generator => :get_default_email
|
291
|
-
field :issue_dir, :
|
321
|
+
field :issue_dir, :prompt => "Directory to store issues state in", :default => "bugs"
|
292
322
|
|
293
323
|
def user; "#{name} <#{email}>" end
|
294
324
|
|
295
325
|
def get_default_name
|
296
326
|
require 'etc'
|
297
327
|
|
298
|
-
name =
|
299
|
-
|
328
|
+
name = if ENV["USER"]
|
329
|
+
pwent = Etc.getpwnam ENV["USER"]
|
330
|
+
pwent ? pwent.gecos.split(/,/).first : nil
|
331
|
+
end
|
332
|
+
name || "Ditz User"
|
300
333
|
end
|
301
334
|
|
302
335
|
def get_default_email
|
data/lib/model.rb
CHANGED
@@ -90,6 +90,10 @@ class ModelObject
|
|
90
90
|
@serialized_values[name] = o
|
91
91
|
end
|
92
92
|
|
93
|
+
define_method "__serialized_#{name}" do
|
94
|
+
@serialized_values[name]
|
95
|
+
end
|
96
|
+
|
93
97
|
define_method name do
|
94
98
|
return @values[name] if @values.member?(name)
|
95
99
|
@values[name] = deserialized_form_of name, @serialized_values[name]
|
@@ -110,6 +114,14 @@ class ModelObject
|
|
110
114
|
returning YAML::load_file(fn) do |o|
|
111
115
|
raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
|
112
116
|
o.pathname = fn if o.respond_to? :pathname=
|
117
|
+
|
118
|
+
o.class.fields.each do |f, opts|
|
119
|
+
m = "__serialized_#{f}"
|
120
|
+
if opts[:multi] && o.send(m).nil?
|
121
|
+
$stderr.puts "Warning: corrected nil multi-field #{f}"
|
122
|
+
o.send "#{m}=", []
|
123
|
+
end
|
124
|
+
end
|
113
125
|
end
|
114
126
|
end
|
115
127
|
|
@@ -170,37 +182,83 @@ class ModelObject
|
|
170
182
|
def changed!; @changed = true end
|
171
183
|
def unchanged!; @changed = false end
|
172
184
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
ask_multiline q
|
185
|
+
class << self
|
186
|
+
## creates the object, prompting the user when necessary. can take
|
187
|
+
## a :with => { hash } parameter for pre-filling model fields.
|
188
|
+
##
|
189
|
+
## can also take a :defaults_from => obj parameter for pre-filling model
|
190
|
+
## fields from another object with (some of) those fields. kinda like a
|
191
|
+
## bizarre interactive copy constructor.
|
192
|
+
def create_interactively opts={}
|
193
|
+
o = self.new
|
194
|
+
generator_args = opts[:args] || []
|
195
|
+
@fields.each do |name, field_opts|
|
196
|
+
val = if opts[:with] && opts[:with][name]
|
197
|
+
opts[:with][name]
|
198
|
+
elsif(found, x = generate_field_value(o, field_opts, generator_args)) && found
|
199
|
+
x
|
189
200
|
else
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
201
|
+
q = field_opts[:prompt] || name.to_s.capitalize
|
202
|
+
if field_opts[:multiline]
|
203
|
+
## multiline options currently aren't allowed to have a default
|
204
|
+
## value, so just ask.
|
205
|
+
ask_multiline q
|
206
|
+
else
|
207
|
+
default = if opts[:defaults_from] && opts[:defaults_from].respond_to?(name) && (x = opts[:defaults_from].send(name))
|
208
|
+
x
|
209
|
+
else
|
210
|
+
default = generate_field_default o, field_opts, generator_args
|
211
|
+
end
|
212
|
+
ask q, :default => default
|
196
213
|
end
|
197
|
-
|
198
|
-
ask q, :default => default
|
199
214
|
end
|
215
|
+
o.send "#{name}=", val
|
216
|
+
end
|
217
|
+
o
|
218
|
+
end
|
219
|
+
|
220
|
+
## creates the object, filling in fields from 'vals', and throwing a
|
221
|
+
## ModelError when it can't find all the requisite fields
|
222
|
+
def create generator_args, vals={}
|
223
|
+
o = self.new
|
224
|
+
@fields.each do |name, opts|
|
225
|
+
val = if vals[name]
|
226
|
+
vals[name]
|
227
|
+
elsif(found, x = generate_field_value(o, opts, generator_args)) && found
|
228
|
+
x
|
229
|
+
else
|
230
|
+
raise ModelError, "missing required field #{name}"
|
231
|
+
end
|
232
|
+
o.send "#{name}=", val
|
233
|
+
end
|
234
|
+
o
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
## get the value for a field if it can be automatically determined
|
240
|
+
## returns [success, value] (because a successful value can be ni)
|
241
|
+
def generate_field_value o, opts, args
|
242
|
+
if opts[:generator].is_a? Proc
|
243
|
+
[true, opts[:generator].call(*args)]
|
244
|
+
elsif opts[:generator]
|
245
|
+
[true, o.send(opts[:generator], *args)]
|
246
|
+
elsif opts[:ask] == false # nil counts as true here
|
247
|
+
[true, opts[:default] || (opts[:multi] ? [] : nil)]
|
248
|
+
else
|
249
|
+
[false, nil]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def generate_field_default o, opts, args
|
254
|
+
if opts[:default_generator].is_a? Proc
|
255
|
+
opts[:default_generator].call(*args)
|
256
|
+
elsif opts[:default_generator]
|
257
|
+
o.send opts[:default_generator], *args
|
258
|
+
elsif opts[:default]
|
259
|
+
opts[:default]
|
200
260
|
end
|
201
|
-
o.send("#{name}=", val)
|
202
261
|
end
|
203
|
-
o
|
204
262
|
end
|
205
263
|
end
|
206
264
|
|
data/lib/operator.rb
CHANGED
@@ -9,9 +9,10 @@ class Operator
|
|
9
9
|
def method_to_op meth; meth.to_s.gsub("_", "-") end
|
10
10
|
def op_to_method op; op.gsub("-", "_").intern end
|
11
11
|
|
12
|
-
def operation method, desc, *args_spec
|
12
|
+
def operation method, desc, *args_spec, &options_blk
|
13
13
|
@operations ||= {}
|
14
|
-
@operations[method] = { :desc => desc, :args_spec => args_spec
|
14
|
+
@operations[method] = { :desc => desc, :args_spec => args_spec,
|
15
|
+
:options_blk => options_blk }
|
15
16
|
end
|
16
17
|
|
17
18
|
def operations
|
@@ -19,6 +20,11 @@ class Operator
|
|
19
20
|
end
|
20
21
|
def has_operation? op; @operations.member? op_to_method(op) end
|
21
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
|
+
|
22
28
|
## parse the specs, and the commandline arguments, and resolve them. does
|
23
29
|
## typechecking but currently doesn't check for open_issues actually being
|
24
30
|
## open, unstarted_issues being unstarted, etc. probably will check for
|
@@ -50,7 +56,13 @@ class Operator
|
|
50
56
|
when :issue, :open_issue, :unstarted_issue, :started_issue, :assigned_issue
|
51
57
|
## issue completion sticks the title on there, so this will strip it off
|
52
58
|
valr = val.sub(/\A(\w+-\d+)_.*$/,'\1')
|
53
|
-
project.
|
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
|
54
66
|
when :release, :unreleased_release
|
55
67
|
if val == "unassigned"
|
56
68
|
:unassigned
|
@@ -96,7 +108,13 @@ class Operator
|
|
96
108
|
|
97
109
|
def do op, project, config, args
|
98
110
|
meth = self.class.op_to_method(op)
|
111
|
+
|
112
|
+
# Parse options, removing them from args
|
113
|
+
opts = self.class.build_opts meth, args
|
99
114
|
built_args = self.class.build_args project, meth, args
|
115
|
+
|
116
|
+
built_args.unshift opts if opts
|
117
|
+
|
100
118
|
send meth, project, config, *built_args
|
101
119
|
end
|
102
120
|
|
@@ -109,8 +127,15 @@ class Operator
|
|
109
127
|
Project.create_interactively
|
110
128
|
end
|
111
129
|
|
112
|
-
operation :help, "List all registered commands", :maybe_command
|
113
|
-
|
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
|
114
139
|
return help_single(command) if command
|
115
140
|
puts <<EOS
|
116
141
|
Ditz commands:
|
@@ -145,11 +170,32 @@ Usage: ditz #{name} #{args}
|
|
145
170
|
EOS
|
146
171
|
end
|
147
172
|
|
148
|
-
|
149
|
-
def
|
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
|
150
197
|
issue = Issue.create_interactively(:args => [config, project]) or return
|
151
|
-
|
152
|
-
issue.log "created", config.user, comment
|
198
|
+
issue.log "created", config.user, get_comment(opts)
|
153
199
|
project.add_issue issue
|
154
200
|
project.assign_issue_names!
|
155
201
|
puts "Added issue #{issue.name}."
|
@@ -161,12 +207,14 @@ EOS
|
|
161
207
|
puts "Dropped #{issue.name}. Note that other issue names may have changed."
|
162
208
|
end
|
163
209
|
|
164
|
-
operation :add_release, "Add a release", :maybe_name
|
165
|
-
|
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
|
166
215
|
puts "Adding release #{maybe_name}." if maybe_name
|
167
216
|
release = Release.create_interactively(:args => [project, config], :with => { :name => maybe_name }) or return
|
168
|
-
|
169
|
-
release.log "created", config.user, comment
|
217
|
+
release.log "created", config.user, get_comment(opts)
|
170
218
|
project.add_release release
|
171
219
|
puts "Added release #{release.name}."
|
172
220
|
end
|
@@ -178,13 +226,15 @@ EOS
|
|
178
226
|
puts "Added component #{component.name}."
|
179
227
|
end
|
180
228
|
|
181
|
-
operation :add_reference, "Add a reference to an issue", :issue
|
182
|
-
|
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
|
183
234
|
puts "Adding a reference to #{issue.name}: #{issue.title}."
|
184
235
|
reference = ask "Reference"
|
185
|
-
comment = ask_multiline "Comments" unless $opts[:no_comment]
|
186
236
|
issue.add_reference reference
|
187
|
-
issue.log "added reference #{issue.references.size}", config.user,
|
237
|
+
issue.log "added reference #{issue.references.size}", config.user, get_comment(opts)
|
188
238
|
puts "Added reference to #{issue.name}."
|
189
239
|
end
|
190
240
|
|
@@ -208,7 +258,9 @@ EOS
|
|
208
258
|
"%2d/%2d %s" % [nc, num, type.to_s.pluralize(num, false)]
|
209
259
|
end
|
210
260
|
|
211
|
-
bar = if r
|
261
|
+
bar = if r == :unassigned
|
262
|
+
""
|
263
|
+
elsif r.released?
|
212
264
|
"(released)"
|
213
265
|
elsif issues.empty?
|
214
266
|
"(no issues)"
|
@@ -248,33 +300,44 @@ EOS
|
|
248
300
|
join
|
249
301
|
end
|
250
302
|
|
251
|
-
def todo_list_for issues
|
303
|
+
def todo_list_for issues, opts={}
|
252
304
|
return if issues.empty?
|
253
305
|
name_len = issues.max_of { |i| i.name.length }
|
254
306
|
issues.map do |i|
|
255
|
-
sprintf "%s %#{name_len}s: %s
|
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"
|
256
310
|
end.join
|
257
311
|
end
|
258
312
|
|
259
|
-
|
260
|
-
|
261
|
-
|
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
|
262
327
|
end
|
263
328
|
|
264
|
-
operation :
|
265
|
-
|
266
|
-
|
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]
|
267
334
|
end
|
268
335
|
|
269
336
|
def actually_do_todo project, config, releases, full
|
270
337
|
releases ||= project.unreleased_releases + [:unassigned]
|
271
338
|
releases = [*releases]
|
272
339
|
releases.each do |r|
|
273
|
-
|
274
|
-
puts "Unassigned:"
|
275
|
-
else
|
276
|
-
puts "Version #{r.name} (#{r.status}):"
|
277
|
-
end
|
340
|
+
puts r == :unassigned ? "Unassigned:" : "#{r.name} (#{r.status}):"
|
278
341
|
issues = project.issues_for_release r
|
279
342
|
issues = issues.select { |i| i.open? } unless full
|
280
343
|
puts(todo_list_for(issues.sort_by { |i| i.sort_order }) || "No open issues.")
|
@@ -287,33 +350,42 @@ EOS
|
|
287
350
|
ScreenView.new(project, config).render_issue issue
|
288
351
|
end
|
289
352
|
|
290
|
-
operation :start, "Start work on an issue", :unstarted_issue
|
291
|
-
|
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
|
292
358
|
puts "Starting work on issue #{issue.name}: #{issue.title}."
|
293
|
-
|
294
|
-
issue.start_work config.user, comment
|
359
|
+
issue.start_work config.user, get_comment(opts)
|
295
360
|
puts "Recorded start of work for #{issue.name}."
|
296
361
|
end
|
297
362
|
|
298
|
-
operation :stop, "Stop work on an issue", :started_issue
|
299
|
-
|
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
|
300
368
|
puts "Stopping work on issue #{issue.name}: #{issue.title}."
|
301
|
-
|
302
|
-
issue.stop_work config.user, comment
|
369
|
+
issue.stop_work config.user, get_comment(opts)
|
303
370
|
puts "Recorded work stop for #{issue.name}."
|
304
371
|
end
|
305
372
|
|
306
|
-
operation :close, "Close an issue", :open_issue
|
307
|
-
|
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
|
308
378
|
puts "Closing issue #{issue.name}: #{issue.title}."
|
309
379
|
disp = ask_for_selection Issue::DISPOSITIONS, "disposition", lambda { |x| Issue::DISPOSITION_STRINGS[x] || x.to_s }
|
310
|
-
|
311
|
-
issue.close disp, config.user, comment
|
380
|
+
issue.close disp, config.user, get_comment(opts)
|
312
381
|
puts "Closed issue #{issue.name} with disposition #{issue.disposition_string}."
|
313
382
|
end
|
314
383
|
|
315
|
-
operation :assign, "Assign an issue to a release", :issue, :maybe_release
|
316
|
-
|
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
|
317
389
|
if maybe_release && maybe_release.name == issue.release
|
318
390
|
raise Error, "issue #{issue.name} already assigned to release #{issue.release}"
|
319
391
|
end
|
@@ -324,8 +396,6 @@ EOS
|
|
324
396
|
"not assigned to any release."
|
325
397
|
end
|
326
398
|
|
327
|
-
puts "Assigning to release #{maybe_release.name}." if maybe_release
|
328
|
-
|
329
399
|
release = maybe_release || begin
|
330
400
|
releases = project.releases.sort_by { |r| (r.release_time || 0).to_i }
|
331
401
|
releases -= [releases.find { |r| r.name == issue.release }] if issue.release
|
@@ -337,13 +407,15 @@ EOS
|
|
337
407
|
end
|
338
408
|
end
|
339
409
|
end
|
340
|
-
|
341
|
-
issue.assign_to_release release, config.user, comment
|
410
|
+
issue.assign_to_release release, config.user, get_comment(opts)
|
342
411
|
puts "Assigned #{issue.name} to #{release.name}."
|
343
412
|
end
|
344
413
|
|
345
|
-
operation :set_component, "Set an issue's component", :issue, :maybe_component
|
346
|
-
|
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
|
347
419
|
puts "Changing the component of issue #{issue.name}: #{issue.title}."
|
348
420
|
|
349
421
|
if project.components.size == 1
|
@@ -359,8 +431,7 @@ EOS
|
|
359
431
|
components -= [components.find { |r| r.name == issue.component }] if issue.component
|
360
432
|
ask_for_selection(components, "component") { |r| r.name }
|
361
433
|
end
|
362
|
-
|
363
|
-
issue.assign_to_component component, config.user, comment
|
434
|
+
issue.assign_to_component component, config.user, get_comment(opts)
|
364
435
|
oldname = issue.name
|
365
436
|
project.assign_issue_names!
|
366
437
|
puts <<EOS
|
@@ -369,18 +440,23 @@ have changed as well.
|
|
369
440
|
EOS
|
370
441
|
end
|
371
442
|
|
372
|
-
operation :unassign, "Unassign an issue from any releases", :assigned_issue
|
373
|
-
|
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
|
374
448
|
puts "Unassigning issue #{issue.name}: #{issue.title}."
|
375
|
-
|
376
|
-
issue.unassign config.user, comment
|
449
|
+
issue.unassign config.user, get_comment(opts)
|
377
450
|
puts "Unassigned #{issue.name}."
|
378
451
|
end
|
379
452
|
|
380
|
-
operation :comment, "Comment on an issue", :issue
|
381
|
-
|
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
|
382
458
|
puts "Commenting on issue #{issue.name}: #{issue.title}."
|
383
|
-
comment =
|
459
|
+
comment = get_comment opts
|
384
460
|
if comment.blank?
|
385
461
|
puts "Empty comment, aborted."
|
386
462
|
else
|
@@ -398,10 +474,12 @@ EOS
|
|
398
474
|
end
|
399
475
|
end
|
400
476
|
|
401
|
-
operation :release, "Release a release", :unreleased_release
|
402
|
-
|
403
|
-
|
404
|
-
|
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)
|
405
483
|
puts "Release #{release.name} released!"
|
406
484
|
end
|
407
485
|
|
@@ -484,8 +562,12 @@ EOS
|
|
484
562
|
puts "Archived to #{dir}."
|
485
563
|
end
|
486
564
|
|
487
|
-
operation :edit, "Edit an issue", :issue
|
488
|
-
|
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
|
489
571
|
data = { :title => issue.title, :description => issue.desc,
|
490
572
|
:reporter => issue.reporter }
|
491
573
|
|
@@ -496,12 +578,11 @@ EOS
|
|
496
578
|
return
|
497
579
|
end
|
498
580
|
|
499
|
-
comment = ask_multiline "Comments" unless $opts[:no_comment]
|
500
|
-
|
501
581
|
begin
|
502
582
|
edits = YAML.load_file fn
|
503
|
-
|
504
|
-
|
583
|
+
comment = opts[:silent] ? nil : get_comment(opts)
|
584
|
+
if issue.change edits, config.user, comment, opts[:silent]
|
585
|
+
puts "Change recorded."
|
505
586
|
else
|
506
587
|
puts "No changes."
|
507
588
|
end
|