ditz 0.4 → 0.5
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 +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
|