ruby-issues 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/bin/issues +551 -0
  2. data/bin/todos +551 -0
  3. metadata +96 -0
data/bin/issues ADDED
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ #======================================================================================================================#
4
+
5
+ require 'rubygems'
6
+ require 'fattr'
7
+ require 'io/console'
8
+ require 'SecureRandom'
9
+ require 'PrettyComment'
10
+ require 'trollop'
11
+ require 'tempfile'
12
+ require 'yaml'
13
+
14
+ #YAML::ENGINE.yamler = 'psych'
15
+
16
+ #======================================================================================================================#
17
+
18
+
19
+
20
+ #======================================================================================================================#
21
+
22
+ class LogEntry
23
+ fattr :date, :message
24
+
25
+
26
+ #----------------------------------------------------------------------------------------------------------------------#
27
+
28
+ def initialize(message)
29
+ @date = Time.new
30
+ @message = message.dup
31
+ end
32
+ end
33
+
34
+
35
+ #======================================================================================================================#
36
+
37
+ class Issue
38
+ fattr :id, :created, :type, :title, :description, :status
39
+ attr_accessor :history
40
+
41
+
42
+ #----------------------------------------------------------------------------------------------------------------------#
43
+
44
+ def initialize
45
+ @history = []
46
+ end
47
+
48
+
49
+ #----------------------------------------------------------------------------------------------------------------------#
50
+
51
+ def self.createNewIssue(title, type="bug")
52
+ newIssue = Issue.new
53
+ newIssue.id = SecureRandom.hex.force_encoding("UTF-8")
54
+ newIssue.created = Time.new
55
+ newIssue.title = title
56
+ newIssue.status = "open"
57
+ newIssue.type = type
58
+ newIssue.history = [LogEntry.new("Issue created")]
59
+ newIssue
60
+ end
61
+
62
+
63
+ #----------------------------------------------------------------------------------------------------------------------#
64
+
65
+ def copy_from(a_issue)
66
+ self.class.fattrs.each do |a|
67
+ a_issue.send(a) && self.send(a, a_issue.send(a).dup)
68
+ end
69
+ end
70
+
71
+
72
+ #----------------------------------------------------------------------------------------------------------------------#
73
+
74
+ def log(message)
75
+ @history ||= []
76
+ @history << LogEntry.new(message)
77
+ end
78
+
79
+
80
+
81
+ #----------------------------------------------------------------------------------------------------------------------#
82
+
83
+ def format_verbose
84
+ puts PrettyComment.separator
85
+ puts PrettyComment.comment("#{@id[0,6]} #{@type.capitalize} (#{@status}) #{@created.to_s[0,16]}")
86
+ puts PrettyComment.comment("")
87
+ puts PrettyComment.format_line(@title, "#", false, "#")
88
+
89
+ if @description
90
+ puts PrettyComment.sub_heading("Description:")
91
+ @description.split("\n").each do |l|
92
+ puts PrettyComment.format_line(@description, "#", false, "#")
93
+ end
94
+ end
95
+
96
+ if @history && @history.count > 0
97
+ puts PrettyComment.sub_heading("Log:")
98
+ @history.each { |l| puts PrettyComment.format_line("#{l.message}", "# #{l.date.to_s[0,16]}", true, "#", "#") }
99
+ end
100
+
101
+ puts PrettyComment.separator
102
+ puts
103
+ puts
104
+ end
105
+
106
+
107
+ #----------------------------------------------------------------------------------------------------------------------#
108
+
109
+ def format_list
110
+ puts PrettyComment.format_line(@title, "#{short_id} (#{@type[0,1].capitalize})", true)
111
+ end
112
+
113
+
114
+ #----------------------------------------------------------------------------------------------------------------------#
115
+
116
+ def edit_description
117
+ file = Tempfile.new('issues')
118
+ file.write(@description)
119
+ file.close
120
+ system("$EDITOR #{file.path}")
121
+
122
+ file.open
123
+ new_description = file.read
124
+
125
+ if new_description != @description
126
+ @description = new_description.dup
127
+ return true
128
+ end
129
+
130
+ return false
131
+ end
132
+
133
+
134
+ #----------------------------------------------------------------------------------------------------------------------#
135
+
136
+ def edit_all
137
+ edit_file = file = Tempfile.new('issues')
138
+ file.write(self.to_yaml)
139
+ file.close
140
+
141
+ system("$EDITOR #{file.path}")
142
+
143
+ file.open
144
+
145
+ if (file.read != self.to_yaml)
146
+ new_issue = YAML::load_file(file.path)
147
+ self.copy_from(new_issue)
148
+ return true
149
+ end
150
+
151
+ return false
152
+ end
153
+
154
+
155
+ #----------------------------------------------------------------------------------------------------------------------#
156
+
157
+ def short_id
158
+ @id[0,6]
159
+ end
160
+
161
+ end
162
+
163
+
164
+ #======================================================================================================================#
165
+ # Main Program Logic
166
+ #======================================================================================================================#
167
+
168
+ class IssuesDb
169
+ fattr :issues_array
170
+
171
+ #----------------------------------------------------------------------------------------------------------------------#
172
+
173
+ def initialize(database_file)
174
+ @database_file = database_file
175
+ @issues_array = FileTest.exists?(database_file) && YAML.load_file(database_file) || []
176
+ end
177
+
178
+
179
+ #----------------------------------------------------------------------------------------------------------------------#
180
+
181
+ def select_issues(&select_proc)
182
+ return @issues_array.select(&select_proc)
183
+ end
184
+
185
+
186
+ #----------------------------------------------------------------------------------------------------------------------#
187
+
188
+ def select_issue(&select_proc)
189
+ result = select_issues(&select_proc)
190
+
191
+ if result.count == 1
192
+ return result[0]
193
+
194
+ elsif result.count > 1
195
+ puts "Found more than one issue that match this query:"
196
+ result.each{|i| puts("#{i.id} #{i.title}")}
197
+ exit
198
+
199
+ else
200
+ puts "Error: No issue found for query."
201
+ exit
202
+ end
203
+
204
+ nil
205
+ end
206
+
207
+
208
+ #----------------------------------------------------------------------------------------------------------------------#
209
+
210
+ def has_issue(issue_id)
211
+ @issues_array.any? { |issue| issue.id.start_with?(issue_id) }
212
+ end
213
+
214
+
215
+ #----------------------------------------------------------------------------------------------------------------------#
216
+
217
+ def save_db()
218
+ FileTest.exists?('.issues') || Dir.mkdir('.issues')
219
+ File.open(@database_file, 'w' ) { |out| YAML.dump(@issues_array, out) }
220
+ end
221
+
222
+
223
+ #----------------------------------------------------------------------------------------------------------------------#
224
+
225
+ def determine_issue_type(opts)
226
+ issue_types = %w{bug improvement task}
227
+ issue_type = nil
228
+ issue_types.each { |t| opts[t.to_sym] == true && issue_type = t }
229
+
230
+ issue_type && (return issue_type)
231
+
232
+ case opts[:title]
233
+ when /\b(improve|implement)/i
234
+ "improvement"
235
+ when /\b(fix|bug|crash)/i
236
+ "bug"
237
+ else
238
+ "task"
239
+ end
240
+ end
241
+
242
+
243
+ #----------------------------------------------------------------------------------------------------------------------#
244
+
245
+ def create_issue(opts)
246
+ type = determine_issue_type(opts)
247
+ new_issue = Issue.createNewIssue(opts[:title], type)
248
+ @issues_array << new_issue
249
+ save_db()
250
+ puts "Created issue #{new_issue.short_id} #{new_issue.title}"
251
+ end
252
+
253
+
254
+ #----------------------------------------------------------------------------------------------------------------------#
255
+
256
+ def list_issues(opts)
257
+ if opts[:issue_id]
258
+ list_issue(opts[:issue_id])
259
+ else
260
+ list_proc = opts[:verbose] ? "format_verbose" : "format_list"
261
+
262
+ issue_types = %w{bug improvement task}
263
+ did_select_issue_types = opts.any? { |key,value| issue_types.include?(key.to_s.chomp('s')) && value == true }
264
+ did_select_issue_types && issue_types.delete_if { |issue_type| opts["#{issue_type}s".to_sym] == false }
265
+
266
+ status_regex = opts[:all] ? /^(open|resolved|duplicate|wontfix)/ : /^open$/
267
+
268
+ issues = @issues_array.select { |issue| (status_regex =~ issue.status) && issue_types.include?(issue.type) }
269
+ issues.each {|issue| issue.method(list_proc).call}
270
+ end
271
+ end
272
+
273
+
274
+ #----------------------------------------------------------------------------------------------------------------------#
275
+
276
+ def list_issue(issue_id)
277
+ issue = select_issue {|i| i.id.start_with?(issue_id) }
278
+ issue.format_verbose()
279
+ end
280
+
281
+
282
+ #----------------------------------------------------------------------------------------------------------------------#
283
+
284
+ def resolve_issues(opts)
285
+ issue_id = opts[:issue_id]
286
+ resolved_issue = select_issue{|i| i.id.start_with?(issue_id) && i.status == "open"}
287
+
288
+ duplicate_of_id = opts[:cmd] == "duplicate" && select_issue {|i| i.id.start_with?(opts[:duplicate_of_id]) }.id
289
+
290
+ status, message =
291
+ case opts[:cmd]
292
+ when "resolve"
293
+ ["resolved", "Resolved"]
294
+ when "wontfix"
295
+ ["wontfix", "Won't fix"]
296
+ when "duplicate"
297
+ ["duplicate(#{duplicate_of_id})", "Duplicate"]
298
+ end
299
+
300
+ resolved_issue.status = status
301
+ resolved_issue.log "Changed status to #{status}"
302
+
303
+ message = "#{message} issue #{resolved_issue.short_id} #{resolved_issue.title}"
304
+ puts message
305
+
306
+
307
+ save_db()
308
+ opts[:commit] && exec("git commit -a -m \"#{message}\"")
309
+ end
310
+
311
+
312
+ #----------------------------------------------------------------------------------------------------------------------#
313
+
314
+ def delete_issues(opts)
315
+ delete_issues = []
316
+
317
+ opts[:issue_ids].each do |issue_id|
318
+ delete_issues << select_issue{|i| i.id.start_with?(issue_id)}
319
+ end
320
+
321
+ puts "Ok to delete issues: "
322
+ delete_issues.each { |issue| puts "#{issue.short_id} \"#{issue.title}\"" }
323
+ puts "[y/N]"
324
+
325
+ answer = STDIN.getch
326
+
327
+ if /y/i =~ answer
328
+ @issues_array -= delete_issues
329
+ save_db()
330
+
331
+ if delete_issues.count == 1
332
+ puts "Removed issue #{delete_issues[0].short_id} \"#{delete_issues[0].title}\" from database."
333
+ else
334
+ puts "Removed issues "
335
+ delete_issues.each { |issue| puts "#{issue.short_id} \"#{issue.title}\"" }
336
+ puts "from database."
337
+ end
338
+ end
339
+
340
+ end
341
+
342
+
343
+ #----------------------------------------------------------------------------------------------------------------------#
344
+
345
+ def edit_issue(opts)
346
+ issue_id = opts[:issue_id]
347
+ issue = select_issue { |i| i.id.start_with?(issue_id) }
348
+
349
+ did_change_issue = false
350
+
351
+ if (opts[:description])
352
+ did_change_issue = issue.edit_description && issue.log("Edited description")
353
+ else
354
+ did_change_issue = issue.edit_all && issue.log("Edited issue")
355
+ end
356
+
357
+ did_change_issue && save_db()
358
+ end
359
+
360
+
361
+ #----------------------------------------------------------------------------------------------------------------------#
362
+
363
+ def set_type(opts)
364
+ new_type = opts[:new_type]
365
+ opts[:issue_ids].each do |issue_id|
366
+ issue = select_issue { |i| i.id.start_with?(issue_id) }
367
+ if issue.type != new_type
368
+ issue.type = new_type
369
+ issue.log("Changed typed to #{issue.type}")
370
+ issue.format_list
371
+ else
372
+ puts "Issue #{issue.short_id} already of type #{issue.type}."
373
+ end
374
+ end
375
+
376
+ save_db()
377
+ end
378
+
379
+
380
+ #----------------------------------------------------------------------------------------------------------------------#
381
+
382
+
383
+ end
384
+
385
+
386
+ #======================================================================================================================#
387
+ # Command Line Parsing
388
+ #======================================================================================================================#
389
+
390
+ def get_issue_ids(num_ids, usage)
391
+ result = []
392
+ count = num_ids >= 0 ? num_ids : ARGV.count
393
+
394
+ begin
395
+ count.times do
396
+ ARGV.count > 0 && /^\h{1,32}$/ =~ ARGV[0] || raise
397
+ result << ARGV.shift
398
+ end
399
+
400
+ result.count == 1 && num_ids > 0 ? result[0] : result
401
+
402
+ rescue
403
+ abort("Usage: issues #{usage}")
404
+ end
405
+ end
406
+
407
+
408
+ #----------------------------------------------------------------------------------------------------------------------#
409
+
410
+ EXECUTABLE_NAME=File.basename($0)
411
+ DATABASE_NAME= ".issues/" << EXECUTABLE_NAME << ".yaml"
412
+
413
+ SUB_COMMANDS = {
414
+ "list" => "list issues",
415
+ "create" => "create a new issue",
416
+ "resolve" => "set status of issue to \"resolved\"",
417
+ "wontfix" => "set status of issue to \"won't fix\"",
418
+ "duplicate" => "mark issue as duplicate of another issue",
419
+ "edit" => "edit an existing issue",
420
+ "delete" => "delete an issue",
421
+ "set-type" => "set the type of an issue"}
422
+
423
+ LeftFieldLength =
424
+ SUB_COMMANDS.collect { |key, value| key.length }.max
425
+
426
+ SubCommandHelp =
427
+ SUB_COMMANDS.collect {|key,value| " #{key.ljust(LeftFieldLength)} #{value}"}.join("\n")
428
+
429
+
430
+ global_opts = Trollop::options do
431
+ banner <<-EOL
432
+ issues: lightweight distributed issue management.
433
+
434
+ Usage:
435
+ ------
436
+ issues [<command>] [<options] [<args>]
437
+
438
+ Commands are:
439
+ -------------
440
+ #{SubCommandHelp}
441
+
442
+ Global Options:
443
+ ---------------
444
+ EOL
445
+ stop_on SUB_COMMANDS.keys
446
+ end
447
+
448
+
449
+ cmd = ARGV.shift # get the subcommand
450
+ cmd ||= 'list'
451
+
452
+ cmd_opts = {}
453
+
454
+ if cmd == 'list'
455
+ cmd_opts =
456
+ Trollop::options do
457
+ opt :all, "list all issues", :short => 'a'
458
+ opt :newest, "list newest issues first"
459
+ opt :oldest, "list oldest issues first"
460
+ opt :verbose, "verbose list of issues", :short => 'v'
461
+ opt :bugs, "list bugs", :short => 'b'
462
+ opt :improvements, "list improvements", :short => 'i'
463
+ opt :tasks, "list tasks", :short => 't'
464
+ end
465
+
466
+ ARGV.count > 0 && cmd_opts[:issue_id] = get_issue_ids(1, "list ID")
467
+
468
+ elsif cmd == "create"
469
+ cmd_opts =
470
+ Trollop::options do
471
+ opt :bug, "create a bug", :short => 'b'
472
+ opt :improvement, "create an improvement", :short => 'i'
473
+ opt :task, "create a task", :short => 't'
474
+ end
475
+ cmd_opts[:title] = ARGV.shift || Trollop::die( "Please enter a title for the new issue!")
476
+
477
+
478
+ elsif cmd == "resolve" || cmd == "wontfix" || cmd == "duplicate"
479
+ cmd_opts =
480
+ Trollop::options do
481
+ opt :commit, "do a git commit", :short => 'c'
482
+ end
483
+
484
+ if cmd == "duplicate"
485
+ cmd_opts[:issue_id], cmd_opts[:duplicate_of_id] = get_issue_ids(2, "duplicate ID(issue) ID(duplicate of)")
486
+ else
487
+ cmd_opts[:issue_id] = get_issue_ids(1, "#{cmd} [-c] ID")
488
+ end
489
+
490
+
491
+ elsif cmd == "edit"
492
+ cmd_opts =
493
+ Trollop::options do
494
+ opt :description, "edit the issue description", :short => 'd'
495
+ end
496
+ cmd_opts[:issue_id] = get_issue_ids(1, "edit ID")
497
+
498
+
499
+ elsif cmd == "set-type"
500
+ Trollop::options do
501
+ banner <<-EOL
502
+ Usage:
503
+ ------
504
+ issues set-type {bug|improvement|task} ID
505
+
506
+ Options:
507
+ --------
508
+ EOL
509
+ end
510
+
511
+ new_type = ARGV.shift
512
+ %w{bug improvement task}.include?(new_type) || Trollop::die("Please specify one of [bug, improvement, task] as new issue type")
513
+ cmd_opts[:new_type] = new_type
514
+ ARGV.count > 0 && cmd_opts[:issue_ids] = get_issue_ids(-1, "set-type {bug|improvement|task} ID")
515
+
516
+
517
+ elsif cmd == "delete"
518
+ cmd_opts[:issue_ids] = get_issue_ids(-1, "#{cmd} ID")
519
+
520
+ else
521
+ Trollop::die "unknown command #{cmd.inspect}"
522
+ end
523
+
524
+
525
+ cmd_opts[:cmd] = cmd
526
+
527
+
528
+ #======================================================================================================================#
529
+ # Main
530
+ #======================================================================================================================#
531
+
532
+ Issues = IssuesDb.new(DATABASE_NAME)
533
+
534
+ case cmd
535
+ when "create"
536
+ Issues.create_issue(cmd_opts)
537
+ when "list"
538
+ Issues.list_issues(cmd_opts)
539
+ when "resolve", "wontfix", "duplicate"
540
+ Issues.resolve_issues(cmd_opts)
541
+ when "edit"
542
+ Issues.edit_issue(cmd_opts)
543
+ when "set-type"
544
+ Issues.set_type(cmd_opts)
545
+ when "delete"
546
+ Issues.delete_issues(cmd_opts)
547
+ end
548
+
549
+
550
+ #======================================================================================================================#
551
+
data/bin/todos ADDED
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+ #======================================================================================================================#
4
+
5
+ require 'rubygems'
6
+ require 'fattr'
7
+ require 'io/console'
8
+ require 'SecureRandom'
9
+ require 'PrettyComment'
10
+ require 'trollop'
11
+ require 'tempfile'
12
+ require 'yaml'
13
+
14
+ #YAML::ENGINE.yamler = 'psych'
15
+
16
+ #======================================================================================================================#
17
+
18
+
19
+
20
+ #======================================================================================================================#
21
+
22
+ class LogEntry
23
+ fattr :date, :message
24
+
25
+
26
+ #----------------------------------------------------------------------------------------------------------------------#
27
+
28
+ def initialize(message)
29
+ @date = Time.new
30
+ @message = message.dup
31
+ end
32
+ end
33
+
34
+
35
+ #======================================================================================================================#
36
+
37
+ class Issue
38
+ fattr :id, :created, :type, :title, :description, :status
39
+ attr_accessor :history
40
+
41
+
42
+ #----------------------------------------------------------------------------------------------------------------------#
43
+
44
+ def initialize
45
+ @history = []
46
+ end
47
+
48
+
49
+ #----------------------------------------------------------------------------------------------------------------------#
50
+
51
+ def self.createNewIssue(title, type="bug")
52
+ newIssue = Issue.new
53
+ newIssue.id = SecureRandom.hex.force_encoding("UTF-8")
54
+ newIssue.created = Time.new
55
+ newIssue.title = title
56
+ newIssue.status = "open"
57
+ newIssue.type = type
58
+ newIssue.history = [LogEntry.new("Issue created")]
59
+ newIssue
60
+ end
61
+
62
+
63
+ #----------------------------------------------------------------------------------------------------------------------#
64
+
65
+ def copy_from(a_issue)
66
+ self.class.fattrs.each do |a|
67
+ a_issue.send(a) && self.send(a, a_issue.send(a).dup)
68
+ end
69
+ end
70
+
71
+
72
+ #----------------------------------------------------------------------------------------------------------------------#
73
+
74
+ def log(message)
75
+ @history ||= []
76
+ @history << LogEntry.new(message)
77
+ end
78
+
79
+
80
+
81
+ #----------------------------------------------------------------------------------------------------------------------#
82
+
83
+ def format_verbose
84
+ puts PrettyComment.separator
85
+ puts PrettyComment.comment("#{@id[0,6]} #{@type.capitalize} (#{@status}) #{@created.to_s[0,16]}")
86
+ puts PrettyComment.comment("")
87
+ puts PrettyComment.format_line(@title, "#", false, "#")
88
+
89
+ if @description
90
+ puts PrettyComment.sub_heading("Description:")
91
+ @description.split("\n").each do |l|
92
+ puts PrettyComment.format_line(@description, "#", false, "#")
93
+ end
94
+ end
95
+
96
+ if @history && @history.count > 0
97
+ puts PrettyComment.sub_heading("Log:")
98
+ @history.each { |l| puts PrettyComment.format_line("#{l.message}", "# #{l.date.to_s[0,16]}", true, "#", "#") }
99
+ end
100
+
101
+ puts PrettyComment.separator
102
+ puts
103
+ puts
104
+ end
105
+
106
+
107
+ #----------------------------------------------------------------------------------------------------------------------#
108
+
109
+ def format_list
110
+ puts PrettyComment.format_line(@title, "#{short_id} (#{@type[0,1].capitalize})", true)
111
+ end
112
+
113
+
114
+ #----------------------------------------------------------------------------------------------------------------------#
115
+
116
+ def edit_description
117
+ file = Tempfile.new('issues')
118
+ file.write(@description)
119
+ file.close
120
+ system("$EDITOR #{file.path}")
121
+
122
+ file.open
123
+ new_description = file.read
124
+
125
+ if new_description != @description
126
+ @description = new_description.dup
127
+ return true
128
+ end
129
+
130
+ return false
131
+ end
132
+
133
+
134
+ #----------------------------------------------------------------------------------------------------------------------#
135
+
136
+ def edit_all
137
+ edit_file = file = Tempfile.new('issues')
138
+ file.write(self.to_yaml)
139
+ file.close
140
+
141
+ system("$EDITOR #{file.path}")
142
+
143
+ file.open
144
+
145
+ if (file.read != self.to_yaml)
146
+ new_issue = YAML::load_file(file.path)
147
+ self.copy_from(new_issue)
148
+ return true
149
+ end
150
+
151
+ return false
152
+ end
153
+
154
+
155
+ #----------------------------------------------------------------------------------------------------------------------#
156
+
157
+ def short_id
158
+ @id[0,6]
159
+ end
160
+
161
+ end
162
+
163
+
164
+ #======================================================================================================================#
165
+ # Main Program Logic
166
+ #======================================================================================================================#
167
+
168
+ class IssuesDb
169
+ fattr :issues_array
170
+
171
+ #----------------------------------------------------------------------------------------------------------------------#
172
+
173
+ def initialize(database_file)
174
+ @database_file = database_file
175
+ @issues_array = FileTest.exists?(database_file) && YAML.load_file(database_file) || []
176
+ end
177
+
178
+
179
+ #----------------------------------------------------------------------------------------------------------------------#
180
+
181
+ def select_issues(&select_proc)
182
+ return @issues_array.select(&select_proc)
183
+ end
184
+
185
+
186
+ #----------------------------------------------------------------------------------------------------------------------#
187
+
188
+ def select_issue(&select_proc)
189
+ result = select_issues(&select_proc)
190
+
191
+ if result.count == 1
192
+ return result[0]
193
+
194
+ elsif result.count > 1
195
+ puts "Found more than one issue that match this query:"
196
+ result.each{|i| puts("#{i.id} #{i.title}")}
197
+ exit
198
+
199
+ else
200
+ puts "Error: No issue found for query."
201
+ exit
202
+ end
203
+
204
+ nil
205
+ end
206
+
207
+
208
+ #----------------------------------------------------------------------------------------------------------------------#
209
+
210
+ def has_issue(issue_id)
211
+ @issues_array.any? { |issue| issue.id.start_with?(issue_id) }
212
+ end
213
+
214
+
215
+ #----------------------------------------------------------------------------------------------------------------------#
216
+
217
+ def save_db()
218
+ FileTest.exists?('.issues') || Dir.mkdir('.issues')
219
+ File.open(@database_file, 'w' ) { |out| YAML.dump(@issues_array, out) }
220
+ end
221
+
222
+
223
+ #----------------------------------------------------------------------------------------------------------------------#
224
+
225
+ def determine_issue_type(opts)
226
+ issue_types = %w{bug improvement task}
227
+ issue_type = nil
228
+ issue_types.each { |t| opts[t.to_sym] == true && issue_type = t }
229
+
230
+ issue_type && (return issue_type)
231
+
232
+ case opts[:title]
233
+ when /\b(improve|implement)/i
234
+ "improvement"
235
+ when /\b(fix|bug|crash)/i
236
+ "bug"
237
+ else
238
+ "task"
239
+ end
240
+ end
241
+
242
+
243
+ #----------------------------------------------------------------------------------------------------------------------#
244
+
245
+ def create_issue(opts)
246
+ type = determine_issue_type(opts)
247
+ new_issue = Issue.createNewIssue(opts[:title], type)
248
+ @issues_array << new_issue
249
+ save_db()
250
+ puts "Created issue #{new_issue.short_id} #{new_issue.title}"
251
+ end
252
+
253
+
254
+ #----------------------------------------------------------------------------------------------------------------------#
255
+
256
+ def list_issues(opts)
257
+ if opts[:issue_id]
258
+ list_issue(opts[:issue_id])
259
+ else
260
+ list_proc = opts[:verbose] ? "format_verbose" : "format_list"
261
+
262
+ issue_types = %w{bug improvement task}
263
+ did_select_issue_types = opts.any? { |key,value| issue_types.include?(key.to_s.chomp('s')) && value == true }
264
+ did_select_issue_types && issue_types.delete_if { |issue_type| opts["#{issue_type}s".to_sym] == false }
265
+
266
+ status_regex = opts[:all] ? /^(open|resolved|duplicate|wontfix)/ : /^open$/
267
+
268
+ issues = @issues_array.select { |issue| (status_regex =~ issue.status) && issue_types.include?(issue.type) }
269
+ issues.each {|issue| issue.method(list_proc).call}
270
+ end
271
+ end
272
+
273
+
274
+ #----------------------------------------------------------------------------------------------------------------------#
275
+
276
+ def list_issue(issue_id)
277
+ issue = select_issue {|i| i.id.start_with?(issue_id) }
278
+ issue.format_verbose()
279
+ end
280
+
281
+
282
+ #----------------------------------------------------------------------------------------------------------------------#
283
+
284
+ def resolve_issues(opts)
285
+ issue_id = opts[:issue_id]
286
+ resolved_issue = select_issue{|i| i.id.start_with?(issue_id) && i.status == "open"}
287
+
288
+ duplicate_of_id = opts[:cmd] == "duplicate" && select_issue {|i| i.id.start_with?(opts[:duplicate_of_id]) }.id
289
+
290
+ status, message =
291
+ case opts[:cmd]
292
+ when "resolve"
293
+ ["resolved", "Resolved"]
294
+ when "wontfix"
295
+ ["wontfix", "Won't fix"]
296
+ when "duplicate"
297
+ ["duplicate(#{duplicate_of_id})", "Duplicate"]
298
+ end
299
+
300
+ resolved_issue.status = status
301
+ resolved_issue.log "Changed status to #{status}"
302
+
303
+ message = "#{message} issue #{resolved_issue.short_id} #{resolved_issue.title}"
304
+ puts message
305
+
306
+
307
+ save_db()
308
+ opts[:commit] && exec("git commit -a -m \"#{message}\"")
309
+ end
310
+
311
+
312
+ #----------------------------------------------------------------------------------------------------------------------#
313
+
314
+ def delete_issues(opts)
315
+ delete_issues = []
316
+
317
+ opts[:issue_ids].each do |issue_id|
318
+ delete_issues << select_issue{|i| i.id.start_with?(issue_id)}
319
+ end
320
+
321
+ puts "Ok to delete issues: "
322
+ delete_issues.each { |issue| puts "#{issue.short_id} \"#{issue.title}\"" }
323
+ puts "[y/N]"
324
+
325
+ answer = STDIN.getch
326
+
327
+ if /y/i =~ answer
328
+ @issues_array -= delete_issues
329
+ save_db()
330
+
331
+ if delete_issues.count == 1
332
+ puts "Removed issue #{delete_issues[0].short_id} \"#{delete_issues[0].title}\" from database."
333
+ else
334
+ puts "Removed issues "
335
+ delete_issues.each { |issue| puts "#{issue.short_id} \"#{issue.title}\"" }
336
+ puts "from database."
337
+ end
338
+ end
339
+
340
+ end
341
+
342
+
343
+ #----------------------------------------------------------------------------------------------------------------------#
344
+
345
+ def edit_issue(opts)
346
+ issue_id = opts[:issue_id]
347
+ issue = select_issue { |i| i.id.start_with?(issue_id) }
348
+
349
+ did_change_issue = false
350
+
351
+ if (opts[:description])
352
+ did_change_issue = issue.edit_description && issue.log("Edited description")
353
+ else
354
+ did_change_issue = issue.edit_all && issue.log("Edited issue")
355
+ end
356
+
357
+ did_change_issue && save_db()
358
+ end
359
+
360
+
361
+ #----------------------------------------------------------------------------------------------------------------------#
362
+
363
+ def set_type(opts)
364
+ new_type = opts[:new_type]
365
+ opts[:issue_ids].each do |issue_id|
366
+ issue = select_issue { |i| i.id.start_with?(issue_id) }
367
+ if issue.type != new_type
368
+ issue.type = new_type
369
+ issue.log("Changed typed to #{issue.type}")
370
+ issue.format_list
371
+ else
372
+ puts "Issue #{issue.short_id} already of type #{issue.type}."
373
+ end
374
+ end
375
+
376
+ save_db()
377
+ end
378
+
379
+
380
+ #----------------------------------------------------------------------------------------------------------------------#
381
+
382
+
383
+ end
384
+
385
+
386
+ #======================================================================================================================#
387
+ # Command Line Parsing
388
+ #======================================================================================================================#
389
+
390
+ def get_issue_ids(num_ids, usage)
391
+ result = []
392
+ count = num_ids >= 0 ? num_ids : ARGV.count
393
+
394
+ begin
395
+ count.times do
396
+ ARGV.count > 0 && /^\h{1,32}$/ =~ ARGV[0] || raise
397
+ result << ARGV.shift
398
+ end
399
+
400
+ result.count == 1 && num_ids > 0 ? result[0] : result
401
+
402
+ rescue
403
+ abort("Usage: issues #{usage}")
404
+ end
405
+ end
406
+
407
+
408
+ #----------------------------------------------------------------------------------------------------------------------#
409
+
410
+ EXECUTABLE_NAME=File.basename($0)
411
+ DATABASE_NAME= ".issues/" << EXECUTABLE_NAME << ".yaml"
412
+
413
+ SUB_COMMANDS = {
414
+ "list" => "list issues",
415
+ "create" => "create a new issue",
416
+ "resolve" => "set status of issue to \"resolved\"",
417
+ "wontfix" => "set status of issue to \"won't fix\"",
418
+ "duplicate" => "mark issue as duplicate of another issue",
419
+ "edit" => "edit an existing issue",
420
+ "delete" => "delete an issue",
421
+ "set-type" => "set the type of an issue"}
422
+
423
+ LeftFieldLength =
424
+ SUB_COMMANDS.collect { |key, value| key.length }.max
425
+
426
+ SubCommandHelp =
427
+ SUB_COMMANDS.collect {|key,value| " #{key.ljust(LeftFieldLength)} #{value}"}.join("\n")
428
+
429
+
430
+ global_opts = Trollop::options do
431
+ banner <<-EOL
432
+ issues: lightweight distributed issue management.
433
+
434
+ Usage:
435
+ ------
436
+ issues [<command>] [<options] [<args>]
437
+
438
+ Commands are:
439
+ -------------
440
+ #{SubCommandHelp}
441
+
442
+ Global Options:
443
+ ---------------
444
+ EOL
445
+ stop_on SUB_COMMANDS.keys
446
+ end
447
+
448
+
449
+ cmd = ARGV.shift # get the subcommand
450
+ cmd ||= 'list'
451
+
452
+ cmd_opts = {}
453
+
454
+ if cmd == 'list'
455
+ cmd_opts =
456
+ Trollop::options do
457
+ opt :all, "list all issues", :short => 'a'
458
+ opt :newest, "list newest issues first"
459
+ opt :oldest, "list oldest issues first"
460
+ opt :verbose, "verbose list of issues", :short => 'v'
461
+ opt :bugs, "list bugs", :short => 'b'
462
+ opt :improvements, "list improvements", :short => 'i'
463
+ opt :tasks, "list tasks", :short => 't'
464
+ end
465
+
466
+ ARGV.count > 0 && cmd_opts[:issue_id] = get_issue_ids(1, "list ID")
467
+
468
+ elsif cmd == "create"
469
+ cmd_opts =
470
+ Trollop::options do
471
+ opt :bug, "create a bug", :short => 'b'
472
+ opt :improvement, "create an improvement", :short => 'i'
473
+ opt :task, "create a task", :short => 't'
474
+ end
475
+ cmd_opts[:title] = ARGV.shift || Trollop::die( "Please enter a title for the new issue!")
476
+
477
+
478
+ elsif cmd == "resolve" || cmd == "wontfix" || cmd == "duplicate"
479
+ cmd_opts =
480
+ Trollop::options do
481
+ opt :commit, "do a git commit", :short => 'c'
482
+ end
483
+
484
+ if cmd == "duplicate"
485
+ cmd_opts[:issue_id], cmd_opts[:duplicate_of_id] = get_issue_ids(2, "duplicate ID(issue) ID(duplicate of)")
486
+ else
487
+ cmd_opts[:issue_id] = get_issue_ids(1, "#{cmd} [-c] ID")
488
+ end
489
+
490
+
491
+ elsif cmd == "edit"
492
+ cmd_opts =
493
+ Trollop::options do
494
+ opt :description, "edit the issue description", :short => 'd'
495
+ end
496
+ cmd_opts[:issue_id] = get_issue_ids(1, "edit ID")
497
+
498
+
499
+ elsif cmd == "set-type"
500
+ Trollop::options do
501
+ banner <<-EOL
502
+ Usage:
503
+ ------
504
+ issues set-type {bug|improvement|task} ID
505
+
506
+ Options:
507
+ --------
508
+ EOL
509
+ end
510
+
511
+ new_type = ARGV.shift
512
+ %w{bug improvement task}.include?(new_type) || Trollop::die("Please specify one of [bug, improvement, task] as new issue type")
513
+ cmd_opts[:new_type] = new_type
514
+ ARGV.count > 0 && cmd_opts[:issue_ids] = get_issue_ids(-1, "set-type {bug|improvement|task} ID")
515
+
516
+
517
+ elsif cmd == "delete"
518
+ cmd_opts[:issue_ids] = get_issue_ids(-1, "#{cmd} ID")
519
+
520
+ else
521
+ Trollop::die "unknown command #{cmd.inspect}"
522
+ end
523
+
524
+
525
+ cmd_opts[:cmd] = cmd
526
+
527
+
528
+ #======================================================================================================================#
529
+ # Main
530
+ #======================================================================================================================#
531
+
532
+ Issues = IssuesDb.new(DATABASE_NAME)
533
+
534
+ case cmd
535
+ when "create"
536
+ Issues.create_issue(cmd_opts)
537
+ when "list"
538
+ Issues.list_issues(cmd_opts)
539
+ when "resolve", "wontfix", "duplicate"
540
+ Issues.resolve_issues(cmd_opts)
541
+ when "edit"
542
+ Issues.edit_issue(cmd_opts)
543
+ when "set-type"
544
+ Issues.set_type(cmd_opts)
545
+ when "delete"
546
+ Issues.delete_issues(cmd_opts)
547
+ end
548
+
549
+
550
+ #======================================================================================================================#
551
+
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-issues
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Wolfgang Steiner
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-20 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: PrettyComment
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 0.1.2
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 0.1.2
30
+ - !ruby/object:Gem::Dependency
31
+ name: trollop
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 1.16.2
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.16.2
46
+ - !ruby/object:Gem::Dependency
47
+ name: fattr
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 2.2.1
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 2.2.1
62
+ description: Issues is a lightweight, git-style command-line issue tracker.
63
+ email: wolfgang.steiner@gmail.com
64
+ executables:
65
+ - issues
66
+ - todos
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - bin/issues
71
+ - bin/todos
72
+ homepage: https://github.com/WolfgangSteiner/Issues
73
+ licenses: []
74
+ post_install_message:
75
+ rdoc_options: []
76
+ require_paths:
77
+ - lib
78
+ required_ruby_version: !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ requirements: []
91
+ rubyforge_project:
92
+ rubygems_version: 1.8.23
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Git-style issue tracker.
96
+ test_files: []