bugzyrb 0.1.0
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/.gitignore +26 -0
- data/LICENSE +20 -0
- data/README.rdoc +97 -0
- data/Rakefile +59 -0
- data/VERSION +1 -0
- data/bin/bugzyrb +4 -0
- data/bugzy.cfg +35 -0
- data/bugzyrb.gemspec +72 -0
- data/lib/bugzyrb.rb +1131 -0
- data/lib/common/cmdapp.rb +280 -0
- data/lib/common/colorconstants.rb +79 -0
- data/lib/common/db.rb +249 -0
- data/lib/common/sed.rb +118 -0
- metadata +158 -0
data/lib/bugzyrb.rb
ADDED
@@ -0,0 +1,1131 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
=begin
|
3
|
+
* Name: bugzyrb.rb
|
4
|
+
* Description: a command line bug tracker uses sqlite3 (port of bugzy.sh)
|
5
|
+
* Author: rkumar
|
6
|
+
* Date: 2010-06-24
|
7
|
+
* License: Ruby License
|
8
|
+
* Now requires subcommand gem
|
9
|
+
|
10
|
+
=end
|
11
|
+
require 'rubygems'
|
12
|
+
$:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
|
13
|
+
require 'common/colorconstants'
|
14
|
+
require 'common/sed'
|
15
|
+
require 'common/cmdapp'
|
16
|
+
require 'subcommand'
|
17
|
+
require 'sqlite3'
|
18
|
+
require 'highline/import'
|
19
|
+
require 'common/db'
|
20
|
+
include ColorConstants
|
21
|
+
include Sed
|
22
|
+
include Cmdapp
|
23
|
+
include Subcommands
|
24
|
+
include Database
|
25
|
+
|
26
|
+
PRI_A = YELLOW + BOLD
|
27
|
+
PRI_B = WHITE + BOLD
|
28
|
+
PRI_C = GREEN + BOLD
|
29
|
+
PRI_D = CYAN + BOLD
|
30
|
+
VERSION = "0.0.0"
|
31
|
+
DATE = "2010-06-24"
|
32
|
+
APPNAME = File.basename($0)
|
33
|
+
AUTHOR = "rkumar"
|
34
|
+
|
35
|
+
class Bugzy
|
36
|
+
# This class is responsible for all todo task related functionality.
|
37
|
+
#
|
38
|
+
# == Create a file
|
39
|
+
#
|
40
|
+
# $ bugzyrb init
|
41
|
+
#
|
42
|
+
# The above creates a bugzy.sqlite file
|
43
|
+
#
|
44
|
+
# Adding a task is the first operation.
|
45
|
+
# $ bugzyrb add "Create a project in rubyforge"
|
46
|
+
# $ bugzyrb add "Update Rakefile with project name"
|
47
|
+
#
|
48
|
+
# == List tasks
|
49
|
+
# To list open/unstarted tasks:
|
50
|
+
# $ bugzyrb
|
51
|
+
# To list closed tasks also:
|
52
|
+
# $ bugzyrb --show-all
|
53
|
+
#
|
54
|
+
# If you are located elsewhere, give directory name:
|
55
|
+
# $ bugzyrb -d ~/
|
56
|
+
#
|
57
|
+
# == Close a task (mark as done)
|
58
|
+
# $ bugzyrb status close 1
|
59
|
+
#
|
60
|
+
# == Add priority
|
61
|
+
# $ bugzyrb pri A 2
|
62
|
+
#
|
63
|
+
# For more:
|
64
|
+
# $ bugzyrb --help
|
65
|
+
# $ bugzyrb --show-actions
|
66
|
+
#
|
67
|
+
# == TODO:
|
68
|
+
#
|
69
|
+
def initialize options, argv
|
70
|
+
|
71
|
+
@options = options
|
72
|
+
@argv = argv
|
73
|
+
@file = options[:file]
|
74
|
+
## data is a 2 dim array: rows and fields. It contains each row of the file
|
75
|
+
# as an array of strings. The item number is space padded.
|
76
|
+
@data = []
|
77
|
+
init_vars
|
78
|
+
end
|
79
|
+
def init_vars
|
80
|
+
@app_default_action = "list" # TODO:
|
81
|
+
@file = @app_file_path = @options[:file] || "bugzy.sqlite"
|
82
|
+
#@app_serial_path = File.expand_path("~/serial_numbers")
|
83
|
+
@deleted_path = "todo_deleted.txt"
|
84
|
+
@todo_delim = "\t"
|
85
|
+
@appname = File.basename( Dir.getwd ) #+ ".#{$0}"
|
86
|
+
# in order to support the testing framework
|
87
|
+
t = Time.now
|
88
|
+
#ut = ENV["TODO_TEST_TIME"]
|
89
|
+
#t = Time.at(ut.to_i) if ut
|
90
|
+
@now = t.strftime("%Y-%m-%d %H:%M:%S")
|
91
|
+
@today = t.strftime("%Y-%m-%d")
|
92
|
+
@verbose = @options[:verbose]
|
93
|
+
# menu MENU
|
94
|
+
@valid_type = %w[bug enhancement feature task]
|
95
|
+
@valid_severity = %w[normal critical moderate]
|
96
|
+
@valid_status = %w[open started closed stopped canceled]
|
97
|
+
@valid_priority = %w[P1 P2 P3 P4 P5]
|
98
|
+
$prompt_desc = $prompt_type = $prompt_status = $prompt_severity = $prompt_assigned_to = true
|
99
|
+
$default_priority = nil
|
100
|
+
$default_type = "bug"
|
101
|
+
$default_severity = "normal"
|
102
|
+
$default_status = "open"
|
103
|
+
$default_priority = "P3"
|
104
|
+
$default_assigned_to = ""
|
105
|
+
$default_due = 5 # how many days in advance due date should be
|
106
|
+
#$bare = @options[:bare]
|
107
|
+
# we need to load the cfg file here, if given # , or else in home dir.
|
108
|
+
if @options[:config]
|
109
|
+
load @options[:config]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
%w[type severity status priority].each do |f|
|
113
|
+
eval(
|
114
|
+
"def validate_#{f}(value)
|
115
|
+
@valid_#{f}.include? value
|
116
|
+
end"
|
117
|
+
)
|
118
|
+
end
|
119
|
+
|
120
|
+
# initialize the database in current dir
|
121
|
+
# should we add project and/or component ?
|
122
|
+
# schema - adding created_by for bug and comment and log, but how to get ?
|
123
|
+
# assuming created by will contain email id so longish.
|
124
|
+
def init args=nil
|
125
|
+
die "#{@file} already exist. Please delete if you wish to recreate." if File.exists? @file
|
126
|
+
|
127
|
+
@db = SQLite3::Database.new( @file )
|
128
|
+
sql = <<SQL
|
129
|
+
|
130
|
+
CREATE TABLE bugs (
|
131
|
+
id INTEGER PRIMARY KEY,
|
132
|
+
status VARCHAR(10) NOT NULL,
|
133
|
+
severity VARCHAR(10),
|
134
|
+
type VARCHAR(10),
|
135
|
+
assigned_to VARCHAR(10),
|
136
|
+
start_date DATE default CURRENT_DATE,
|
137
|
+
due_date DATE,
|
138
|
+
comment_count INTEGER default 0,
|
139
|
+
priority VARCHAR(10),
|
140
|
+
title VARCHAR(10) NOT NULL,
|
141
|
+
description TEXT,
|
142
|
+
fix TEXT,
|
143
|
+
created_by VARCHAR(60),
|
144
|
+
project VARCHAR(10),
|
145
|
+
component VARCHAR(10),
|
146
|
+
version VARCHAR(10),
|
147
|
+
date_created DATETIME default CURRENT_TIMESTAMP,
|
148
|
+
date_modified DATETIME default CURRENT_TIMESTAMP);
|
149
|
+
|
150
|
+
CREATE TABLE comments (
|
151
|
+
rowid INTEGER PRIMARY KEY,
|
152
|
+
id INTEGER NOT NULL ,
|
153
|
+
comment TEXT NOT NULL,
|
154
|
+
created_by VARCHAR(60),
|
155
|
+
date_created DATETIME default CURRENT_TIMESTAMP);
|
156
|
+
|
157
|
+
CREATE TABLE log (
|
158
|
+
rowid INTEGER PRIMARY KEY,
|
159
|
+
id INTEGER ,
|
160
|
+
field VARCHAR(15),
|
161
|
+
log TEXT,
|
162
|
+
created_by VARCHAR(60),
|
163
|
+
date_created DATETIME default CURRENT_TIMESTAMP);
|
164
|
+
|
165
|
+
SQL
|
166
|
+
|
167
|
+
ret = @db.execute_batch( sql )
|
168
|
+
# execute batch only returns nil
|
169
|
+
message "#{@file} created." if File.exists? @file
|
170
|
+
text = <<-TEXT
|
171
|
+
If you wish to associate projects and/or components and versions to an issue,
|
172
|
+
please add the same in the database using:
|
173
|
+
|
174
|
+
project add <NAME>
|
175
|
+
component add <PROJECTNAME> <COMPONENT>
|
176
|
+
version add <PROJECTNAME> <VERSION>
|
177
|
+
TEXT
|
178
|
+
message text
|
179
|
+
|
180
|
+
0
|
181
|
+
end
|
182
|
+
def get_db
|
183
|
+
@db ||= DB.new @file
|
184
|
+
end
|
185
|
+
# returns default due date for add or qadd
|
186
|
+
# @return [Date] due date
|
187
|
+
def default_due_date
|
188
|
+
#Date.parse(future_date($default_due).to_s[0..10]) # => converts to a Date object
|
189
|
+
Date.today + $default_due
|
190
|
+
end
|
191
|
+
##
|
192
|
+
# quick add which does not prompt user at all, only title is required on commandline
|
193
|
+
# all other fields will go in as defaults
|
194
|
+
# One may override defaults by specifying options
|
195
|
+
def qadd args
|
196
|
+
die "Title required by qadd" if args.nil? or args.empty?
|
197
|
+
db = get_db
|
198
|
+
body = {}
|
199
|
+
body['title'] = args.join " "
|
200
|
+
body['type'] = @options[:type] || $default_type
|
201
|
+
body['severity'] = @options[:severity] || $default_severity
|
202
|
+
body['status'] = @options[:status] || $default_status
|
203
|
+
body['priority'] = @options[:priority] || $default_priority
|
204
|
+
body['assigned_to'] = @options[:assigned_to]
|
205
|
+
#comment_count = 0
|
206
|
+
#body['description = nil
|
207
|
+
#fix = nil
|
208
|
+
body['start_date'] = @now
|
209
|
+
body['due_date'] = default_due_date
|
210
|
+
#rowid = db.bugs_insert(status, severity, type, assigned_to, start_date, due_date, comment_count, priority, title, description, fix)
|
211
|
+
rowid = db.table_insert_hash("bugs", body)
|
212
|
+
puts "Issue #{rowid} created"
|
213
|
+
type = body['type']
|
214
|
+
title = body['title']
|
215
|
+
logid = db.sql_logs_insert rowid, "create", "#{rowid} #{type}: #{title}"
|
216
|
+
body["id"] = rowid
|
217
|
+
mail_issue body
|
218
|
+
0
|
219
|
+
end
|
220
|
+
|
221
|
+
##
|
222
|
+
# add an issue or bug
|
223
|
+
# @params [Array] text of bug (ARGV), will be concatenated into single string
|
224
|
+
# @return [0,1] success or fail
|
225
|
+
# TODO: users should be able to switch on or off globals, and pass / change defaults
|
226
|
+
# TODO: reading environ ENV and config file.
|
227
|
+
def add args
|
228
|
+
db = get_db
|
229
|
+
if args.empty?
|
230
|
+
print "Enter a short summary: "
|
231
|
+
STDOUT.flush
|
232
|
+
text = gets.chomp
|
233
|
+
if text.empty?
|
234
|
+
exit ERRCODE
|
235
|
+
end
|
236
|
+
else
|
237
|
+
text = args.join " "
|
238
|
+
end
|
239
|
+
# convert actual newline to C-a. slash n's are escapes so echo -e does not muck up.
|
240
|
+
#atitle=$( echo "$atitle" | tr -cd '\40-\176' )
|
241
|
+
text.tr! "\n", ''
|
242
|
+
title = text
|
243
|
+
desc = nil
|
244
|
+
if $prompt_desc
|
245
|
+
message "Enter a detailed description (. to exit): "
|
246
|
+
desc = get_lines
|
247
|
+
end
|
248
|
+
message "You entered #{desc}"
|
249
|
+
type = $default_type || "bug"
|
250
|
+
severity = $default_severity || "normal"
|
251
|
+
status = $default_status || "open"
|
252
|
+
priority = $default_priority || "P3"
|
253
|
+
if $prompt_type
|
254
|
+
type = _choice("Select type:", %w[bug enhancement feature task] )
|
255
|
+
message "You selected #{type}"
|
256
|
+
end
|
257
|
+
if $prompt_severity
|
258
|
+
severity = _choice("Select severity:", %w[normal critical moderate] )
|
259
|
+
message "You selected #{severity}"
|
260
|
+
end
|
261
|
+
if $prompt_status
|
262
|
+
status = _choice("Select status:", %w[open started closed stopped canceled] )
|
263
|
+
message "You selected #{status}"
|
264
|
+
end
|
265
|
+
if $prompt_assigned_to
|
266
|
+
message "Assign to:"
|
267
|
+
assigned_to = $stdin.gets.chomp
|
268
|
+
message "You selected #{assigned_to}"
|
269
|
+
else
|
270
|
+
assigned_to = $default_assigned_to
|
271
|
+
end
|
272
|
+
project = component = version = nil
|
273
|
+
# project
|
274
|
+
if $use_project
|
275
|
+
project = user_input('project', $prompt_project, nil, $valid_project, $default_project)
|
276
|
+
end
|
277
|
+
if $use_component
|
278
|
+
component = user_input('component', $prompt_component, nil, $valid_component, $default_component)
|
279
|
+
end
|
280
|
+
if $use_version
|
281
|
+
version = user_input('version', $prompt_version, nil, $valid_version, $default_version)
|
282
|
+
end
|
283
|
+
|
284
|
+
start_date = @now
|
285
|
+
due_date = default_due_date
|
286
|
+
comment_count = 0
|
287
|
+
priority ||= "P3"
|
288
|
+
description = desc
|
289
|
+
fix = nil #"Some long text"
|
290
|
+
#date_created = @now
|
291
|
+
#date_modified = @now
|
292
|
+
body = {}
|
293
|
+
body["title"]=title
|
294
|
+
body["description"]=description
|
295
|
+
body["type"]=type
|
296
|
+
body["start_date"]=start_date.to_s
|
297
|
+
body["due_date"]=due_date.to_s
|
298
|
+
body["priority"]=priority
|
299
|
+
body["severity"]=severity
|
300
|
+
body["assigned_to"]=assigned_to
|
301
|
+
body["created_by"] = $default_user
|
302
|
+
# only insert if its wanted by user
|
303
|
+
body["project"]=project if $use_project
|
304
|
+
body["component"]=component if $use_component
|
305
|
+
body["version"]=version if $use_version
|
306
|
+
|
307
|
+
#rowid = db.bugs_insert(status, severity, type, assigned_to, start_date, due_date, comment_count, priority, title, description, fix)
|
308
|
+
rowid = db.table_insert_hash("bugs", body)
|
309
|
+
puts "Issue #{rowid} created"
|
310
|
+
logid = db.sql_logs_insert rowid, "create", "#{rowid} #{type}: #{title}"
|
311
|
+
# send an email of some sort needs improbement FIXME
|
312
|
+
#printable = %w[ title description status severity type assigned_to start_date due_date priority fix ]
|
313
|
+
body["id"] = rowid
|
314
|
+
mail_issue body
|
315
|
+
|
316
|
+
0
|
317
|
+
end
|
318
|
+
def mail_issue row, emailid=nil
|
319
|
+
emailid ||= $default_user
|
320
|
+
body = <<TEXT
|
321
|
+
Id : #{row['id']}
|
322
|
+
Title : #{row['title']}
|
323
|
+
Description : #{row['description']}
|
324
|
+
Type : #{row['type']}
|
325
|
+
Start Date : #{row['start_date']}
|
326
|
+
Due Date : #{row['due_date']}
|
327
|
+
Priority : #{row['priority']}
|
328
|
+
Severity : #{row['severity']}
|
329
|
+
Assigned To : #{row['assigned_to']}
|
330
|
+
TEXT
|
331
|
+
title = "#{row['id']}: #{row['title']} "
|
332
|
+
require 'tempfile'
|
333
|
+
temp = Tempfile.new "bugzy"
|
334
|
+
File.open(temp,"w"){ |f| f.write body }
|
335
|
+
|
336
|
+
#cmd = %Q{ echo "#{body}" | mail -s "#{title}" "#{emailid}" }
|
337
|
+
# cat is not portable please change
|
338
|
+
cmd = %Q{ cat #{temp.path} | mail -s "#{title}" "#{emailid}" }
|
339
|
+
|
340
|
+
$stderr.puts "executing: #{cmd}"
|
341
|
+
unless system(cmd)
|
342
|
+
$stderr.puts "Error executing #{cmd}"
|
343
|
+
$stderr.puts $?
|
344
|
+
end
|
345
|
+
|
346
|
+
end
|
347
|
+
##
|
348
|
+
# view details of a single issue/bug
|
349
|
+
# @param [Array] ARGV, first element is issue number
|
350
|
+
# If no arg supplied then shows highest entry
|
351
|
+
def view args
|
352
|
+
db = get_db
|
353
|
+
id = args[0].nil? ? db.max_bug_id : args[0]
|
354
|
+
db, row = validate_id id
|
355
|
+
die "No data found for #{id}" unless row
|
356
|
+
puts "[#{row['type']} \##{row['id']}] #{row['title']}"
|
357
|
+
puts row['description']
|
358
|
+
puts
|
359
|
+
comment_count = 0
|
360
|
+
#puts row
|
361
|
+
row.each_pair { |name, val|
|
362
|
+
next if name == "project" && !$use_project
|
363
|
+
next if name == "version" && !$use_version
|
364
|
+
next if name == "component" && !$use_component
|
365
|
+
comment_count = val.to_i if name == "comment_count"
|
366
|
+
n = sprintf("%-15s", name);
|
367
|
+
puts "#{n} : #{val}"
|
368
|
+
}
|
369
|
+
puts
|
370
|
+
if comment_count > 0
|
371
|
+
puts "Comments :"
|
372
|
+
db.select_where "comments", "id", id do |r|
|
373
|
+
#puts r.join(" | ")
|
374
|
+
puts "(#{r['date_created']}) [ #{r['created_by']} ] #{r['comment']}"
|
375
|
+
#pp r
|
376
|
+
end
|
377
|
+
end
|
378
|
+
puts "Log:"
|
379
|
+
db.select_where "log", "id", id do |r|
|
380
|
+
#puts r.join(" | ")
|
381
|
+
puts "------- (#{r['date_created']}) ------"
|
382
|
+
puts "#{r['field']} [ #{r['created_by']} ] #{r['log']} "
|
383
|
+
#pp r
|
384
|
+
end
|
385
|
+
end
|
386
|
+
## tried out a version of view that uses template replacement
|
387
|
+
# but can't do placement of second column -- it does not come aligned, so forget
|
388
|
+
# NOTE: use rdoc/template instead - can handle arrays
|
389
|
+
def view2 args
|
390
|
+
db = get_db
|
391
|
+
id = args[0].nil? ? db.max_bug_id : args[0]
|
392
|
+
db, row = validate_id id
|
393
|
+
die "No data found for #{id}" unless row
|
394
|
+
t = File.dirname(__FILE__) + "/common/" + "bug.tmpl"
|
395
|
+
template = File::read(t)
|
396
|
+
puts Cmdapp::template_replace(template, row)
|
397
|
+
#puts row
|
398
|
+
#puts "Comments:"
|
399
|
+
t = File.dirname(__FILE__) + "/common/" + "comment.tmpl"
|
400
|
+
template = File::read(t)
|
401
|
+
db.select_where "comments", "id", id do |r|
|
402
|
+
puts Cmdapp::template_replace(template, r)
|
403
|
+
#puts r.join(" | ")
|
404
|
+
#puts "(#{r['date_created']}) #{r['comment']}"
|
405
|
+
#pp r
|
406
|
+
end
|
407
|
+
end
|
408
|
+
def edit args
|
409
|
+
db = get_db
|
410
|
+
id = args[0].nil? ? db.max_bug_id : args[0]
|
411
|
+
row = db.sql_select_rowid "bugs", id
|
412
|
+
die "No data found for #{id}" unless row
|
413
|
+
editable = %w[ status severity type assigned_to start_date due_date priority title description fix ]
|
414
|
+
sel = _choice "Select field to edit", editable
|
415
|
+
print "You chose: #{sel}"
|
416
|
+
old = row[sel]
|
417
|
+
puts " Current value is: #{old}"
|
418
|
+
meth = "ask_#{sel}".to_sym
|
419
|
+
if respond_to? "ask_#{sel}".to_sym
|
420
|
+
str = send(meth, old)
|
421
|
+
else
|
422
|
+
print "Enter value: "
|
423
|
+
str = $stdin.gets.chomp
|
424
|
+
end
|
425
|
+
#str = old if str.nil? or str == ""
|
426
|
+
if str.nil? or str == old
|
427
|
+
message "Operation cancelled."
|
428
|
+
exit 0
|
429
|
+
end
|
430
|
+
message "Updating:"
|
431
|
+
message str
|
432
|
+
db.sql_update "bugs", id, sel, str
|
433
|
+
puts "Updated #{id}"
|
434
|
+
rowid = db.sql_logs_insert id, sel, "[#{id}] updated [#{sel}] with #{str[0..50]}"
|
435
|
+
0
|
436
|
+
end
|
437
|
+
# deletes given issue
|
438
|
+
# @param [Array] id of issue
|
439
|
+
def delete args
|
440
|
+
id = args.shift
|
441
|
+
if @options[:force]
|
442
|
+
db, row = validate_id id, false
|
443
|
+
db.sql_delete_bug id
|
444
|
+
exit 0
|
445
|
+
end
|
446
|
+
db, row = validate_id id, true
|
447
|
+
if agree("Delete this issue? ")
|
448
|
+
db.sql_delete_bug id
|
449
|
+
else
|
450
|
+
message "Operation cancelled"
|
451
|
+
end
|
452
|
+
0
|
453
|
+
end
|
454
|
+
def viewlogs args
|
455
|
+
db = get_db
|
456
|
+
id = args[0].nil? ? db.max_bug_id : args[0]
|
457
|
+
row = db.sql_select_rowid "bugs", id
|
458
|
+
die "No data found for #{id}" unless row
|
459
|
+
puts "[#{row['type']} \##{row['id']}] #{row['title']}"
|
460
|
+
puts row['description']
|
461
|
+
puts
|
462
|
+
ctr = 0
|
463
|
+
db.select_where "log", "id", id do |r|
|
464
|
+
ctr += 1
|
465
|
+
puts "(#{r['date_created']}) #{r['field']} \t #{r['log']}"
|
466
|
+
#puts "(#{r['date_created']}) #{r['log']}"
|
467
|
+
end
|
468
|
+
message "No logs found" if ctr == 0
|
469
|
+
0
|
470
|
+
end
|
471
|
+
##
|
472
|
+
# lists issues
|
473
|
+
# @param [Array] argv: containing Strings containing matching or non-matching terms
|
474
|
+
# +term means title should include term
|
475
|
+
# -term means title should not include term
|
476
|
+
# @example
|
477
|
+
# list +testing
|
478
|
+
# list testing
|
479
|
+
# list crash -windows
|
480
|
+
# list -- -linux
|
481
|
+
def list args
|
482
|
+
# lets look at args as search words
|
483
|
+
incl = []
|
484
|
+
excl = []
|
485
|
+
args.each do |e|
|
486
|
+
if e[0] == '+'
|
487
|
+
incl << e[1..-1]
|
488
|
+
elsif e[0] == '-'
|
489
|
+
excl << e[1..-1]
|
490
|
+
else
|
491
|
+
incl << e
|
492
|
+
end
|
493
|
+
end
|
494
|
+
incl = nil if incl.empty?
|
495
|
+
excl = nil if excl.empty?
|
496
|
+
db = get_db
|
497
|
+
#db.run "select * from bugs " do |row|
|
498
|
+
#end
|
499
|
+
fields = "id,status,title,severity,priority,start_date,due_date"
|
500
|
+
if @options[:short]
|
501
|
+
fields = "id,status,title"
|
502
|
+
elsif @options[:long]
|
503
|
+
fields = "id,status,title,severity,priority,due_date,description"
|
504
|
+
end
|
505
|
+
rows = db.run "select #{fields} from bugs "
|
506
|
+
|
507
|
+
if incl
|
508
|
+
incl_str = incl.join "|"
|
509
|
+
r = Regexp.new incl_str
|
510
|
+
rows = rows.select { |row| row['title'] =~ r }
|
511
|
+
end
|
512
|
+
if excl
|
513
|
+
excl_str = excl.join "|"
|
514
|
+
r = Regexp.new excl_str
|
515
|
+
rows = rows.select { |row| row['title'] !~ r }
|
516
|
+
end
|
517
|
+
headings = fields.split ","
|
518
|
+
# if you want to filter output send a delimiter
|
519
|
+
if $bare
|
520
|
+
delim = @options[:delimiter] || "\t"
|
521
|
+
puts headings.join delim
|
522
|
+
rows.each do |e|
|
523
|
+
d = e['description']
|
524
|
+
e['description'] = d.gsub(/\n/," ") if d
|
525
|
+
puts e.join delim
|
526
|
+
end
|
527
|
+
else
|
528
|
+
# pretty output tabular format etc
|
529
|
+
require 'terminal-table/import'
|
530
|
+
#table = table(nil, *rows)
|
531
|
+
table = table(headings, *rows)
|
532
|
+
puts table
|
533
|
+
end
|
534
|
+
end
|
535
|
+
## validate user entered id
|
536
|
+
# All methods should call this first.
|
537
|
+
# @param [Fixnum] id (actually can be String) to validate
|
538
|
+
# @return [Database, #execute] database handle
|
539
|
+
# @return [ResultSet] (arrayfields) data of row retrieved
|
540
|
+
# NOTE: exits (die) if no such row, so if calling in a loop ...
|
541
|
+
def validate_id id, print_header=false
|
542
|
+
db = get_db
|
543
|
+
#id ||= db.max_bug_id # if none supplied get highest - should we do this.
|
544
|
+
# no since the caller will not have id, will bomb later
|
545
|
+
row = db.sql_select_rowid "bugs", id
|
546
|
+
die "No data found for #{id}" unless row
|
547
|
+
if print_header
|
548
|
+
puts "[#{row['type']} \##{row['id']}] #{row['title']}"
|
549
|
+
puts row['description']
|
550
|
+
puts
|
551
|
+
end
|
552
|
+
return db, row
|
553
|
+
end
|
554
|
+
def putxx *args
|
555
|
+
puts "GOT:: #{args}"
|
556
|
+
end
|
557
|
+
def ask_type old=nil
|
558
|
+
type = _choice("Select type:", %w[bug enhancement feature task] )
|
559
|
+
end
|
560
|
+
def ask_severity old=nil
|
561
|
+
severity = _choice("Select severity:", %w[normal critical moderate] )
|
562
|
+
end
|
563
|
+
def ask_status old=nil
|
564
|
+
status = _choice("Select status:", %w[open started closed stopped canceled] )
|
565
|
+
end
|
566
|
+
def ask_priority old=nil
|
567
|
+
priority = _choice("Select priority:", %w[P1 P2 P3 P4 P5] )
|
568
|
+
end
|
569
|
+
def ask_fix old=nil
|
570
|
+
Cmdapp::edit_text old
|
571
|
+
end
|
572
|
+
def ask_description old=nil
|
573
|
+
Cmdapp::edit_text old
|
574
|
+
end
|
575
|
+
##
|
576
|
+
# prompts user for a cooment to be attached to a issue/bug
|
577
|
+
def comment args #id, comment
|
578
|
+
id = args.shift
|
579
|
+
unless id
|
580
|
+
id = ask("Issue Id? ", Integer)
|
581
|
+
end
|
582
|
+
if !args.empty?
|
583
|
+
comment = args.join(" ")
|
584
|
+
else
|
585
|
+
message "Enter a comment (. to exit): "
|
586
|
+
comment = get_lines
|
587
|
+
end
|
588
|
+
die "Operation cancelled" if comment.nil? or comment.empty?
|
589
|
+
message "Comment is: #{comment}."
|
590
|
+
db, row = validate_id id
|
591
|
+
die "No issue found for #{id}" unless row
|
592
|
+
message "Adding comment to #{id}: #{row['title']}"
|
593
|
+
_comment db, id, comment
|
594
|
+
0
|
595
|
+
end
|
596
|
+
# insert comment into database
|
597
|
+
# called from interactive, as well as "close" or others
|
598
|
+
def _comment db, id, text
|
599
|
+
rowid = db.sql_comments_insert id, text
|
600
|
+
puts "Comment #{rowid} created"
|
601
|
+
handle = db.db
|
602
|
+
|
603
|
+
commcount = handle.get_first_value( "select count(id) from comments where id = #{id};" )
|
604
|
+
commcount = commcount.to_i
|
605
|
+
db.sql_update "bugs", id, "comment_count", commcount
|
606
|
+
rowid = db.sql_logs_insert id, "comment",text[0..50]
|
607
|
+
end
|
608
|
+
# prompts user for a fix related to an issue
|
609
|
+
def fix args #id, fix
|
610
|
+
id = args.shift
|
611
|
+
unless id
|
612
|
+
id = ask("Issue Id? ", Integer)
|
613
|
+
end
|
614
|
+
if !args.empty?
|
615
|
+
text = args.join(" ")
|
616
|
+
else
|
617
|
+
message "Enter a fix (. to exit): "
|
618
|
+
text = get_lines
|
619
|
+
end
|
620
|
+
die "Operation cancelled" if text.nil? or text.empty?
|
621
|
+
message "fix is: #{text}."
|
622
|
+
db, row = validate_id id
|
623
|
+
message "Adding fix to #{id}: #{row['title']}"
|
624
|
+
_fix db, id, text
|
625
|
+
0
|
626
|
+
end
|
627
|
+
def _fix db, id, text
|
628
|
+
db.sql_update "bugs", id, "fix", text
|
629
|
+
rowid = db.sql_logs_insert id, "fix", text[0..50]
|
630
|
+
end
|
631
|
+
## internal method to log an action
|
632
|
+
# @param [Fixnum] id
|
633
|
+
# @param [String] column or create/delete for row
|
634
|
+
# @param [String] details such as content added, or content changed
|
635
|
+
def log id, field, text
|
636
|
+
id = args.shift
|
637
|
+
unless id
|
638
|
+
id = ask("Issue Id? ", Integer)
|
639
|
+
end
|
640
|
+
if !args.empty?
|
641
|
+
comment = args.join(" ")
|
642
|
+
else
|
643
|
+
message "Enter a comment (. to exit): "
|
644
|
+
comment = get_lines
|
645
|
+
end
|
646
|
+
die "Operation cancelled" if comment.nil? or comment.empty?
|
647
|
+
message "Comment is: #{comment}."
|
648
|
+
db = get_db
|
649
|
+
row = db.sql_select_rowid "bugs", id
|
650
|
+
die "No issue found for #{id}" unless row
|
651
|
+
message "Adding comment to #{id}: #{row['title']}"
|
652
|
+
rowid = db.sql_logs_insert id, field, log
|
653
|
+
puts "Comment #{rowid} created"
|
654
|
+
0
|
655
|
+
end
|
656
|
+
|
657
|
+
##
|
658
|
+
# change value of given column
|
659
|
+
# This is typically called internally so the new value will be validated.
|
660
|
+
# We can also do a validation against an array
|
661
|
+
# @param [String] column name
|
662
|
+
# @param [String] new value
|
663
|
+
# @param [Array] array of id's to close (argv)
|
664
|
+
# @return [0] for success
|
665
|
+
def change_value field="status", value="closed", args
|
666
|
+
#field = "status"
|
667
|
+
#value = "closed"
|
668
|
+
meth = "validate_#{field}".to_sym
|
669
|
+
if respond_to? meth
|
670
|
+
#bool = send("validate_#{field}".to_sym, value)
|
671
|
+
bool = send(meth, value)
|
672
|
+
die "#{value} is not valid for #{field}" unless bool
|
673
|
+
end
|
674
|
+
args.each do |id|
|
675
|
+
db, row = validate_id id
|
676
|
+
curr_status = row[field]
|
677
|
+
# don't update if already closed
|
678
|
+
if curr_status != value
|
679
|
+
db.sql_update "bugs", id, field, value
|
680
|
+
puts "Updated #{id}"
|
681
|
+
rowid = db.sql_logs_insert id, field, "[#{id}] updated [#{field}] with #{value}"
|
682
|
+
else
|
683
|
+
message "#{id} already #{value}"
|
684
|
+
end
|
685
|
+
_comment(db, id, @options[:comment]) if @options[:comment]
|
686
|
+
_fix(db, id, @options[:fix]) if @options[:fix]
|
687
|
+
end
|
688
|
+
0
|
689
|
+
end
|
690
|
+
# close an issue (changes status of issue/s)
|
691
|
+
# @param [Array] array of id's to close (argv)
|
692
|
+
# @return [0] for success
|
693
|
+
def close args
|
694
|
+
change_value "status", "closed", args
|
695
|
+
0
|
696
|
+
end
|
697
|
+
|
698
|
+
# start an issue (changes status of issue/s)
|
699
|
+
# @param [Array] array of id's to start (argv)
|
700
|
+
# @return [0] for success
|
701
|
+
def start args
|
702
|
+
change_value "status", "started", args
|
703
|
+
0
|
704
|
+
end
|
705
|
+
|
706
|
+
##
|
707
|
+
# get a date in the future giving how many days
|
708
|
+
# @param [Fixnum] how many days in the future
|
709
|
+
# @return [Time] Date object in future
|
710
|
+
# @example
|
711
|
+
# future_date(1).to_s[0..10]; # => creates a string object with only Date part, no time
|
712
|
+
# Date.parse(future_date(1).to_s[0..10]) # => converts to a Date object
|
713
|
+
|
714
|
+
def future_date days=1
|
715
|
+
Time.now() + (24 * 60 * 60 * days)
|
716
|
+
#(Time.now() + (24 * 60 * 60) * days).to_s[0..10];
|
717
|
+
end
|
718
|
+
|
719
|
+
## prompt user for due date, called from edit
|
720
|
+
#def ask_due_date
|
721
|
+
#days = 1
|
722
|
+
#ask("Enter due date? ", Date) {
|
723
|
+
#|q| q.default = future_date(days).to_s[0..10];
|
724
|
+
#q.validate = lambda { |p| Date.parse(p) >= Date.parse(Time.now.to_s) };
|
725
|
+
#q.responses[:not_valid] = "Enter a date greater than today"
|
726
|
+
#}
|
727
|
+
#end
|
728
|
+
# prompt user for due date, called from edit
|
729
|
+
def ask_due_date
|
730
|
+
days = 1
|
731
|
+
today = Date.today
|
732
|
+
ask("Enter due date? ", Date) {
|
733
|
+
|q| q.default = today + days;
|
734
|
+
q.validate = lambda { |p| Date.parse(p) >= today };
|
735
|
+
q.responses[:not_valid] = "Enter a date greater than today"
|
736
|
+
}
|
737
|
+
end
|
738
|
+
|
739
|
+
def ask_start_date
|
740
|
+
ask("Enter start date? ", Date) {
|
741
|
+
#|q| q.default = Time.now.to_s[0..10];
|
742
|
+
|q| q.default = Date.today
|
743
|
+
}
|
744
|
+
end
|
745
|
+
|
746
|
+
def check_file filename=@app_file_path
|
747
|
+
File.exists?(filename) or die "#{filename} does not exist in this dir. Use 'add' to create an item first."
|
748
|
+
end
|
749
|
+
##
|
750
|
+
# colorize each line, if required.
|
751
|
+
# However, we should put the colors in some Map, so it can be changed at configuration level.
|
752
|
+
#
|
753
|
+
def colorize # TODO:
|
754
|
+
colorme = @options[:colorize]
|
755
|
+
@data.each do |r|
|
756
|
+
if @options[:hide_numbering]
|
757
|
+
string = "#{r[1]} "
|
758
|
+
else
|
759
|
+
string = " #{r[0]} #{r[1]} "
|
760
|
+
end
|
761
|
+
if colorme
|
762
|
+
m=string.match(/\(([A-Z])\)/)
|
763
|
+
if m
|
764
|
+
case m[1]
|
765
|
+
when "A", "B", "C", "D"
|
766
|
+
pri = self.class.const_get("PRI_#{m[1]}")
|
767
|
+
#string = "#{YELLOW}#{BOLD}#{string}#{CLEAR}"
|
768
|
+
string = "#{pri}#{string}#{CLEAR}"
|
769
|
+
else
|
770
|
+
string = "#{NORMAL}#{GREEN}#{string}#{CLEAR}"
|
771
|
+
#string = "#{BLUE}\e[6m#{string}#{CLEAR}"
|
772
|
+
#string = "#{BLUE}#{string}#{CLEAR}"
|
773
|
+
end
|
774
|
+
else
|
775
|
+
#string = "#{NORMAL}#{string}#{CLEAR}"
|
776
|
+
# no need to put clear, let it be au natural
|
777
|
+
end
|
778
|
+
end # colorme
|
779
|
+
## since we've added notes, we convert C-a to newline with spaces
|
780
|
+
# so it prints in next line with some neat indentation.
|
781
|
+
string.gsub!('', "\n ")
|
782
|
+
#string.tr! '', "\n"
|
783
|
+
puts string
|
784
|
+
end
|
785
|
+
end
|
786
|
+
# internal method for sorting on reverse of line (status, priority)
|
787
|
+
def sort # TODO:
|
788
|
+
fold_subtasks
|
789
|
+
if @options[:reverse]
|
790
|
+
@data.sort! { |a,b| a[1] <=> b[1] }
|
791
|
+
else
|
792
|
+
@data.sort! { |a,b| b[1] <=> a[1] }
|
793
|
+
end
|
794
|
+
unfold_subtasks
|
795
|
+
end
|
796
|
+
def grep # TODO:
|
797
|
+
r = Regexp.new @options[:grep]
|
798
|
+
#@data = @data.grep r
|
799
|
+
@data = @data.find_all {|i| i[1] =~ r }
|
800
|
+
end
|
801
|
+
|
802
|
+
##
|
803
|
+
# separates args into tag or subcommand and items
|
804
|
+
# This allows user to pass e.g. a priority first and then item list
|
805
|
+
# or item list first and then priority.
|
806
|
+
# This can only be used if the tag or pri or status is non-numeric and the item is numeric.
|
807
|
+
def _separate args, pattern=nil #/^[a-zA-Z]/
|
808
|
+
tag = nil
|
809
|
+
items = []
|
810
|
+
args.each do |arg|
|
811
|
+
if arg =~ /^[0-9\.]+$/
|
812
|
+
items << arg
|
813
|
+
else
|
814
|
+
tag = arg
|
815
|
+
if pattern
|
816
|
+
die "#{@action}: #{arg} appears invalid." if arg !~ pattern
|
817
|
+
end
|
818
|
+
end
|
819
|
+
end
|
820
|
+
items = nil if items.empty?
|
821
|
+
return tag, items
|
822
|
+
end
|
823
|
+
|
824
|
+
def _choice prompt, choices
|
825
|
+
choose do |menu|
|
826
|
+
menu.prompt = prompt
|
827
|
+
menu.choices(*choices) do |n| return n; end
|
828
|
+
end
|
829
|
+
end
|
830
|
+
#
|
831
|
+
# take user input based on value of flag
|
832
|
+
# @param [String] column name
|
833
|
+
# @param [Boolean, Symbol] true, false, :freeform, :choice
|
834
|
+
# @param [String, nil] text to prompt
|
835
|
+
# @param [Array, nil] choices array or nil
|
836
|
+
# @param [Object] default value
|
837
|
+
# @return [String, nil] users choice
|
838
|
+
#
|
839
|
+
# TODO: should we not check for the ask_x methods and call them if present.
|
840
|
+
def user_input column, prompt_flag, prompt_text=nil, choices=nil, default=nil
|
841
|
+
if prompt_flag == true
|
842
|
+
prompt_flag = :freeform
|
843
|
+
prompt_flag = :choice if choices
|
844
|
+
end
|
845
|
+
case prompt_flag
|
846
|
+
when :freeform
|
847
|
+
prompt_text ||= "#{column.capitalize}? "
|
848
|
+
str = ask(prompt_text){ |q| q.default = default if default }
|
849
|
+
return str
|
850
|
+
when :choice
|
851
|
+
prompt_text ||= "Select #{column}:"
|
852
|
+
str = _choice(prompt_text, choices)
|
853
|
+
return str
|
854
|
+
when :multiline, :ml
|
855
|
+
return Cmdapp::edit_text default
|
856
|
+
when false
|
857
|
+
#return nil
|
858
|
+
return default
|
859
|
+
end
|
860
|
+
end
|
861
|
+
def test args=nil
|
862
|
+
if $use_project
|
863
|
+
project = user_input('project', $prompt_project, nil, $valid_project, $default_project)
|
864
|
+
puts project
|
865
|
+
end
|
866
|
+
if $use_component
|
867
|
+
component = user_input('component', $prompt_component, nil, $valid_component, $default_component)
|
868
|
+
puts component
|
869
|
+
end
|
870
|
+
end
|
871
|
+
## prompts user for multiline input
|
872
|
+
# @param [String] text to use as prompt
|
873
|
+
# @return [String, nil] string with newlines or nil (if nothing entered).
|
874
|
+
#
|
875
|
+
def get_lines prompt=nil
|
876
|
+
#prompt ||= "Enter multiple lines, to quit enter . on empty line"
|
877
|
+
#message prompt
|
878
|
+
str = ""
|
879
|
+
while $stdin.gets # reads from STDIN
|
880
|
+
if $_.chomp == "."
|
881
|
+
break
|
882
|
+
end
|
883
|
+
str << $_
|
884
|
+
#puts "Read: #{$_}" # writes to STDOUT
|
885
|
+
end
|
886
|
+
return nil if str == ""
|
887
|
+
return str
|
888
|
+
end
|
889
|
+
|
890
|
+
def self.main args
|
891
|
+
ret = nil
|
892
|
+
begin
|
893
|
+
# http://www.ruby-doc.org/stdlib/libdoc/optparse/rdoc/classes/OptionParser.html
|
894
|
+
require 'optparse'
|
895
|
+
options = {}
|
896
|
+
options[:verbose] = false
|
897
|
+
options[:colorize] = true
|
898
|
+
$bare = false
|
899
|
+
# adding some env variable pickups, so you don't have to keep passing.
|
900
|
+
showall = ENV["TODO_SHOW_ALL"]
|
901
|
+
if showall
|
902
|
+
options[:show_all] = (showall == "0") ? false:true
|
903
|
+
end
|
904
|
+
plain = ENV["TODO_PLAIN"]
|
905
|
+
if plain
|
906
|
+
options[:colorize] = (plain == "0") ? false:true
|
907
|
+
end
|
908
|
+
|
909
|
+
Subcommands::global_options do |opts|
|
910
|
+
opts.banner = "Usage: #{APPNAME} [options] [subcommand [options]]"
|
911
|
+
opts.description = "Todo list manager"
|
912
|
+
#opts.separator ""
|
913
|
+
#opts.separator "Global options are:"
|
914
|
+
opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
|
915
|
+
options[:verbose] = v
|
916
|
+
end
|
917
|
+
opts.on("-c", "--config FILENAME", "config filename path") do |v|
|
918
|
+
v = File.expand_path v
|
919
|
+
options[:config] = v
|
920
|
+
if !File.exists? v
|
921
|
+
die "#{RED}#{v}: no such file #{CLEAR}"
|
922
|
+
end
|
923
|
+
end
|
924
|
+
opts.on("-d DIR", "--dir DIR", "Use bugs file in this directory") do |v|
|
925
|
+
require 'FileUtils'
|
926
|
+
dir = File.expand_path v
|
927
|
+
if File.directory? dir
|
928
|
+
options[:dir] = dir
|
929
|
+
# changing dir is important so that serial_number file is the current one.
|
930
|
+
FileUtils.cd dir
|
931
|
+
else
|
932
|
+
die "#{RED}#{v}: no such directory #{CLEAR}"
|
933
|
+
end
|
934
|
+
end
|
935
|
+
opts.on("--show-actions", "show actions ") do |v|
|
936
|
+
#todo = Bugzy.new(options, ARGV)
|
937
|
+
#todo.help nil - not working now that we've moved to subcommand
|
938
|
+
puts Subcommands::print_actions
|
939
|
+
exit 0
|
940
|
+
end
|
941
|
+
|
942
|
+
opts.on("--version", "Show version") do
|
943
|
+
version = Cmdapp::version_info || VERSION
|
944
|
+
puts "#{APPNAME} version #{version}, #{DATE}"
|
945
|
+
puts "by #{AUTHOR}. This software is under the GPL License."
|
946
|
+
exit 0
|
947
|
+
end
|
948
|
+
# No argument, shows at tail. This will print an options summary.
|
949
|
+
# Try it and see!
|
950
|
+
#opts.on("-h", "--help", "Show this message") do
|
951
|
+
#puts opts
|
952
|
+
#exit 0
|
953
|
+
#end
|
954
|
+
end
|
955
|
+
Subcommands::add_help_option
|
956
|
+
Subcommands::global_options do |opts|
|
957
|
+
opts.separator ""
|
958
|
+
opts.separator "Common Usage:"
|
959
|
+
opts.separator <<TEXT
|
960
|
+
#{APPNAME} add "Text ...."
|
961
|
+
#{APPNAME} list
|
962
|
+
#{APPNAME} start 1
|
963
|
+
#{APPNAME} close 1
|
964
|
+
TEXT
|
965
|
+
end
|
966
|
+
|
967
|
+
Subcommands::command :init do |opts|
|
968
|
+
opts.banner = "Usage: init [options]"
|
969
|
+
opts.description = "Create a datastore (sqlite3) for bugs/issues"
|
970
|
+
end
|
971
|
+
|
972
|
+
Subcommands::command :add, :a do |opts|
|
973
|
+
opts.banner = "Usage: add [options] TEXT"
|
974
|
+
opts.description = "Add a bug/issue."
|
975
|
+
opts.on("-f", "--[no-]force", "force verbosely") do |v|
|
976
|
+
options[:force] = v
|
977
|
+
end
|
978
|
+
opts.on("-P", "--project PROJECTNAME", "name of project ") { |v|
|
979
|
+
options[:project] = v
|
980
|
+
#options[:filter] = true
|
981
|
+
}
|
982
|
+
opts.on("-p", "--priority PRI", "priority code ") { |v|
|
983
|
+
options[:priority] = v
|
984
|
+
}
|
985
|
+
opts.on("-C", "--component COMPONENT", "component name ") { |v|
|
986
|
+
options[:component] = v
|
987
|
+
}
|
988
|
+
opts.on("--severity SEV", "severity code ") { |v|
|
989
|
+
options[:severity] = v
|
990
|
+
}
|
991
|
+
opts.on("-t", "--type TYPE", "type code ") { |v|
|
992
|
+
options[:type] = v
|
993
|
+
}
|
994
|
+
opts.on("--status STATUS", "status code ") { |v|
|
995
|
+
options[:status] = v
|
996
|
+
}
|
997
|
+
opts.on("-a","--assigned-to assignee", "assigned to whom ") { |v|
|
998
|
+
options[:assigned_to] = v
|
999
|
+
}
|
1000
|
+
end
|
1001
|
+
Subcommands::command :qadd, :a do |opts|
|
1002
|
+
opts.banner = "Usage: qadd [options] TITLE"
|
1003
|
+
opts.description = "Add an issue with no prompting"
|
1004
|
+
opts.on("-p", "--priority PRI", "priority code for add") { |v|
|
1005
|
+
options[:priority] = v
|
1006
|
+
}
|
1007
|
+
opts.on("-C", "--component COMPONENT", "component name for add or list") { |v|
|
1008
|
+
options[:component] = v
|
1009
|
+
}
|
1010
|
+
opts.on("--severity SEV", "severity code for add") { |v|
|
1011
|
+
options[:severity] = v
|
1012
|
+
}
|
1013
|
+
opts.on("-t","--type TYPE", "type code for add") { |v|
|
1014
|
+
options[:type] = v
|
1015
|
+
}
|
1016
|
+
opts.on("--status STATUS", "status code for add") { |v|
|
1017
|
+
options[:status] = v
|
1018
|
+
}
|
1019
|
+
opts.on("-a","--assigned-to assignee", "assigned to whom ") { |v|
|
1020
|
+
options[:assigned_to] = v
|
1021
|
+
}
|
1022
|
+
end
|
1023
|
+
Subcommands::command :view do |opts|
|
1024
|
+
opts.banner = "Usage: view [options] ISSUE_NO"
|
1025
|
+
opts.description = "View a given issue"
|
1026
|
+
end
|
1027
|
+
Subcommands::command :edit do |opts|
|
1028
|
+
opts.banner = "Usage: edit [options] ISSUE_NO"
|
1029
|
+
opts.description = "Edit a given issue"
|
1030
|
+
end
|
1031
|
+
Subcommands::command :comment do |opts|
|
1032
|
+
opts.banner = "Usage: comment [options] ISSUE_NO TEXT"
|
1033
|
+
opts.description = "Add comment a given issue"
|
1034
|
+
end
|
1035
|
+
Subcommands::command :test do |opts|
|
1036
|
+
opts.banner = "Usage: test [options] ISSUE_NO TEXT"
|
1037
|
+
opts.description = "Add test a given issue"
|
1038
|
+
end
|
1039
|
+
Subcommands::command :list do |opts|
|
1040
|
+
opts.banner = "Usage: list [options] search options"
|
1041
|
+
opts.description = "list issues"
|
1042
|
+
opts.on("--short", "short listing") { |v|
|
1043
|
+
options[:short] = v
|
1044
|
+
}
|
1045
|
+
opts.on("--long", "long listing") { |v|
|
1046
|
+
options[:long] = v
|
1047
|
+
}
|
1048
|
+
opts.on("-d","--delimiter STR", "listing delimiter") { |v|
|
1049
|
+
options[:delimiter] = v
|
1050
|
+
}
|
1051
|
+
opts.on("-b","--bare", "unformatted listing, for filtering") { |v|
|
1052
|
+
options[:bare] = v
|
1053
|
+
$bare = true
|
1054
|
+
}
|
1055
|
+
end
|
1056
|
+
Subcommands::command :viewlogs do |opts|
|
1057
|
+
opts.banner = "Usage: viewlogs [options] ISSUE_NO"
|
1058
|
+
opts.description = "view logs for an issue"
|
1059
|
+
end
|
1060
|
+
# XXX order of these 2 matters !! reverse and see what happens
|
1061
|
+
Subcommands::command :close, :clo do |opts|
|
1062
|
+
opts.banner = "Usage: clo [options] <ISSUENO>"
|
1063
|
+
opts.description = "Close an issue/s with fix or comment if given"
|
1064
|
+
opts.on("-f", "--fix TEXT", "add a fix while closing") do |v|
|
1065
|
+
options[:fix] = v
|
1066
|
+
end
|
1067
|
+
opts.on("-c", "--comment TEXT", "add a comment while closing") do |v|
|
1068
|
+
options[:comment] = v
|
1069
|
+
end
|
1070
|
+
end
|
1071
|
+
Subcommands::command :start, :sta do |opts|
|
1072
|
+
opts.banner = "Usage: sta [options] <ISSUENO>"
|
1073
|
+
opts.description = "Mark as started an issue/s with comment if given"
|
1074
|
+
#opts.on("-f", "--fix TEXT", "add a fix while closing") do |v|
|
1075
|
+
#options[:fix] = v
|
1076
|
+
#end
|
1077
|
+
opts.on("-c", "--comment TEXT", "add a comment while closing") do |v|
|
1078
|
+
options[:comment] = v
|
1079
|
+
end
|
1080
|
+
end
|
1081
|
+
#Subcommands::command :depri do |opts|
|
1082
|
+
#opts.banner = "Usage: depri [options] <TASK/s>"
|
1083
|
+
#opts.description = "Remove priority of task. \n\t bugzyrb depri <TASK>"
|
1084
|
+
#opts.on("-f", "--[no-]force", "force verbosely") do |v|
|
1085
|
+
#options[:force] = v
|
1086
|
+
#end
|
1087
|
+
#end
|
1088
|
+
Subcommands::command :delete, :del do |opts|
|
1089
|
+
opts.banner = "Usage: delete [options] <TASK/s>"
|
1090
|
+
opts.description = "Delete a task. \n\t bugzyrb delete <TASK>"
|
1091
|
+
opts.on("-f", "--[no-]force", "force - don't prompt") do |v|
|
1092
|
+
options[:force] = v
|
1093
|
+
end
|
1094
|
+
#opts.on("--recursive", "operate on subtasks also") { |v|
|
1095
|
+
#options[:recursive] = v
|
1096
|
+
#}
|
1097
|
+
end
|
1098
|
+
Subcommands::command :status do |opts|
|
1099
|
+
opts.banner = "Usage: status [options] <STATUS> <TASKS>"
|
1100
|
+
opts.description = "Change the status of a task. \t<STATUS> are open closed started pending hold next"
|
1101
|
+
opts.on("--recursive", "operate on subtasks also") { |v|
|
1102
|
+
options[:recursive] = v
|
1103
|
+
}
|
1104
|
+
end
|
1105
|
+
Subcommands::command :tag do |opts|
|
1106
|
+
opts.banner = "Usage: tag <TAG> <TASKS>"
|
1107
|
+
opts.description = "Add a tag to an item/s. "
|
1108
|
+
end
|
1109
|
+
#Subcommands::alias_command :open , "status","open"
|
1110
|
+
#Subcommands::alias_command :close , "status","closed"
|
1111
|
+
cmd = Subcommands::opt_parse()
|
1112
|
+
args.unshift cmd if cmd
|
1113
|
+
|
1114
|
+
if options[:verbose]
|
1115
|
+
p options
|
1116
|
+
print "ARGV: "
|
1117
|
+
p args #ARGV
|
1118
|
+
end
|
1119
|
+
#raise "-f FILENAME is mandatory" unless options[:file]
|
1120
|
+
|
1121
|
+
c = Bugzy.new(options, args)
|
1122
|
+
ret = c.run
|
1123
|
+
ensure
|
1124
|
+
end
|
1125
|
+
return ret
|
1126
|
+
end # main
|
1127
|
+
end # class Bugzy
|
1128
|
+
|
1129
|
+
if __FILE__ == $0
|
1130
|
+
exit Bugzy.main(ARGV)
|
1131
|
+
end
|