ruby-issues 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/bin/issues +376 -159
  3. data/bin/todos +1 -0
  4. metadata +15 -22
  5. data/bin/todos +0 -551
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f6deed01a2a59cd3b896a927a85b51eae83ca73c
4
+ data.tar.gz: 1812367238a2c59990e949bbe4070ec5c217847b
5
+ SHA512:
6
+ metadata.gz: 9b26f17dbae9bfa96b8316fba969aa06366455678addce3bd383e23f6bc18862639ff031d8041b2a69ceae0a6816cdf2a5bfac775817731ba76f19a8b6ba945b
7
+ data.tar.gz: cad3041f6a80b68ecf71e0f8d4048ce552cbc57b34cea295191dd7a0cdfa921e60a813c45e8a4a9ff79f084233a152a0c6a3dfb042a96ebfa2d48bfcc5a3f2e9
data/bin/issues CHANGED
@@ -1,30 +1,56 @@
1
1
  #!/usr/bin/env ruby
2
2
  # encoding: utf-8
3
- #======================================================================================================================#
4
-
3
+ #===========================================================================================
5
4
  require 'rubygems'
6
5
  require 'fattr'
7
6
  require 'io/console'
8
- require 'SecureRandom'
7
+ require 'securerandom'
9
8
  require 'PrettyComment'
10
9
  require 'trollop'
11
- require 'tempfile'
12
10
  require 'yaml'
11
+ require 'git'
13
12
 
14
13
  #YAML::ENGINE.yamler = 'psych'
15
14
 
16
- #======================================================================================================================#
15
+ #===========================================================================================
17
16
 
17
+ def git_get_current_branch()
18
+ current_branch = `git branch`.split(/\n/).find{ |l| l.start_with?("*") }
19
+
20
+ if current_branch
21
+ current_branch =~ /\*\s+(.+)/
22
+ $~[1]
23
+ end
24
+ end
18
25
 
26
+ #-------------------------------------------------------------------------------------------
27
+
28
+ def is_issue_branch()
29
+ (git_get_current_branch() =~ /(feature|improvement|bug|task)\/[a-f0-9]{6}/) != nil
30
+ end
19
31
 
20
- #======================================================================================================================#
32
+ #-------------------------------------------------------------------------------------------
33
+
34
+ def max(a,b)
35
+ a > b ? a : b
36
+ end
37
+
38
+ #-------------------------------------------------------------------------------------------
39
+
40
+ def edit_string(aString)
41
+ tempfile_name = "/tmp/issues-" << SecureRandom.hex.force_encoding("UTF-8")
42
+ File.write(tempfile_name, aString)
43
+ system("$EDITOR #{tempfile_name}")
44
+ new_string = File.read(tempfile_name)
45
+ File.unlink(tempfile_name)
46
+ new_string
47
+ end
48
+
49
+ #===========================================================================================
21
50
 
22
51
  class LogEntry
23
52
  fattr :date, :message
24
53
 
25
-
26
- #----------------------------------------------------------------------------------------------------------------------#
27
-
28
54
  def initialize(message)
29
55
  @date = Time.new
30
56
  @message = message.dup
@@ -32,21 +58,19 @@ class LogEntry
32
58
  end
33
59
 
34
60
 
35
- #======================================================================================================================#
61
+ #===========================================================================================
36
62
 
37
63
  class Issue
38
- fattr :id, :created, :type, :title, :description, :status
64
+ fattr :id, :created, :type, :title, :description, :status, :milestone, :estimate
39
65
  attr_accessor :history
40
66
 
41
-
42
- #----------------------------------------------------------------------------------------------------------------------#
67
+ #-------------------------------------------------------------------------------------------
43
68
 
44
69
  def initialize
45
70
  @history = []
46
71
  end
47
72
 
48
-
49
- #----------------------------------------------------------------------------------------------------------------------#
73
+ #-------------------------------------------------------------------------------------------
50
74
 
51
75
  def self.createNewIssue(title, type="bug")
52
76
  newIssue = Issue.new
@@ -59,100 +83,103 @@ class Issue
59
83
  newIssue
60
84
  end
61
85
 
62
-
63
- #----------------------------------------------------------------------------------------------------------------------#
86
+ #-------------------------------------------------------------------------------------------
64
87
 
65
88
  def copy_from(a_issue)
66
89
  self.class.fattrs.each do |a|
67
- a_issue.send(a) && self.send(a, a_issue.send(a).dup)
90
+ value = a_issue.send(a)
91
+ new_value = value.is_a?(Numeric) || value == nil ? value : value.dup
92
+ a_issue.send(a) && self.send(a, new_value)
68
93
  end
69
94
  end
70
-
71
95
 
72
- #----------------------------------------------------------------------------------------------------------------------#
96
+ #-------------------------------------------------------------------------------------------
73
97
 
74
98
  def log(message)
75
99
  @history ||= []
76
100
  @history << LogEntry.new(message)
77
101
  end
78
102
 
79
-
80
-
81
- #----------------------------------------------------------------------------------------------------------------------#
103
+ #-------------------------------------------------------------------------------------------
82
104
 
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, "#")
105
+ def format_verbose(opts)
106
+ milestone = @milestone ? "#{@milestone}" : "NO MILESTONE"
107
+ estimate = @estimate ? "#{@estimate}h" : "NO ESTIMATE"
108
+ milestone_estimate = "[#{estimate}, #{milestone}]"
109
+
110
+ result = []
111
+ result << PrettyComment.separator
112
+ result << PrettyComment.comment("#{@id[0,6]} #{@type.capitalize} (#{@status}) #{milestone_estimate} #{@created.to_s[0,16]}")
113
+ result << PrettyComment.comment("")
114
+ result << PrettyComment.format_line(@title, "#", false, "#")
88
115
 
89
116
  if @description
90
- puts PrettyComment.sub_heading("Description:")
117
+ result << PrettyComment.sub_heading("Description:")
91
118
  @description.split("\n").each do |l|
92
- puts PrettyComment.format_line(@description, "#", false, "#")
119
+ result << PrettyComment.format_line(l, "#", false, "#")
93
120
  end
94
121
  end
95
122
 
96
123
  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, "#", "#") }
124
+ result << PrettyComment.sub_heading("Log:")
125
+ @history.each { |l| result << PrettyComment.format_line("#{l.message}", "# #{l.date.to_s[0,16]}", true, "#", "#") }
99
126
  end
100
127
 
101
- puts PrettyComment.separator
102
- puts
103
- puts
128
+ result << PrettyComment.separator
129
+ result << ""
130
+ result << ""
131
+
132
+ result.join("\n")
104
133
  end
105
134
 
135
+ #-------------------------------------------------------------------------------------------
106
136
 
107
- #----------------------------------------------------------------------------------------------------------------------#
137
+ def format_list(opts)
138
+ entry = @title.dup
139
+ info_string = "#{short_id} (#{@type[0,1].capitalize})"
108
140
 
109
- def format_list
110
- puts PrettyComment.format_line(@title, "#{short_id} (#{@type[0,1].capitalize})", true)
111
- end
141
+ if opts[:list_milestone] == true
142
+ width = opts[:milestone_column_width]
143
+ info_string << " " << (@milestone ? @milestone.ljust(width) : "UNASSIGNED".ljust(width))
144
+ end
112
145
 
146
+ if opts[:list_estimate] == true
147
+ info_string << " " << (@estimate ? sprintf("%.2fh", @estimate).rjust(4) : " ")
148
+ end
113
149
 
114
- #----------------------------------------------------------------------------------------------------------------------#
150
+ #suffix = estimate_milestone.count > 0 ? "[#{estimate_milestone.join(' => ')}]" : ""
151
+
152
+ PrettyComment.format_line(entry, info_string, true)
153
+ end
115
154
 
116
- def edit_description
117
- file = Tempfile.new('issues')
118
- file.write(@description)
119
- file.close
120
- system("$EDITOR #{file.path}")
155
+ #-------------------------------------------------------------------------------------------
121
156
 
122
- file.open
123
- new_description = file.read
157
+ def edit_description
158
+ new_description = edit_string(@description)
124
159
 
125
160
  if new_description != @description
126
161
  @description = new_description.dup
127
162
  return true
128
163
  end
129
-
164
+
130
165
  return false
131
166
  end
132
167
 
133
-
134
- #----------------------------------------------------------------------------------------------------------------------#
168
+ #-------------------------------------------------------------------------------------------
135
169
 
136
170
  def edit_all
137
- edit_file = file = Tempfile.new('issues')
138
- file.write(self.to_yaml)
139
- file.close
171
+ original_yaml = self.to_yaml
172
+ new_yaml = edit_string(original_yaml)
140
173
 
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)
174
+ if (new_yaml != original_yaml)
175
+ self.copy_from(YAML::parse(new_string))
148
176
  return true
149
177
  end
150
178
 
151
179
  return false
152
180
  end
153
181
 
154
-
155
- #----------------------------------------------------------------------------------------------------------------------#
182
+ #-------------------------------------------------------------------------------------------
156
183
 
157
184
  def short_id
158
185
  @id[0,6]
@@ -160,30 +187,27 @@ class Issue
160
187
 
161
188
  end
162
189
 
163
-
164
- #======================================================================================================================#
165
- # Main Program Logic
166
- #======================================================================================================================#
190
+ #===========================================================================================
191
+ # Main Program Logic
192
+ #===========================================================================================
167
193
 
168
194
  class IssuesDb
169
195
  fattr :issues_array
170
196
 
171
- #----------------------------------------------------------------------------------------------------------------------#
197
+ #-------------------------------------------------------------------------------------------
172
198
 
173
199
  def initialize(database_file)
174
200
  @database_file = database_file
175
201
  @issues_array = FileTest.exists?(database_file) && YAML.load_file(database_file) || []
176
202
  end
177
203
 
178
-
179
- #----------------------------------------------------------------------------------------------------------------------#
204
+ #-------------------------------------------------------------------------------------------
180
205
 
181
206
  def select_issues(&select_proc)
182
207
  return @issues_array.select(&select_proc)
183
208
  end
184
209
 
185
-
186
- #----------------------------------------------------------------------------------------------------------------------#
210
+ #-------------------------------------------------------------------------------------------
187
211
 
188
212
  def select_issue(&select_proc)
189
213
  result = select_issues(&select_proc)
@@ -204,34 +228,32 @@ class IssuesDb
204
228
  nil
205
229
  end
206
230
 
207
-
208
- #----------------------------------------------------------------------------------------------------------------------#
231
+ #-------------------------------------------------------------------------------------------
209
232
 
210
233
  def has_issue(issue_id)
211
234
  @issues_array.any? { |issue| issue.id.start_with?(issue_id) }
212
235
  end
213
236
 
214
-
215
- #----------------------------------------------------------------------------------------------------------------------#
237
+ #-------------------------------------------------------------------------------------------
216
238
 
217
239
  def save_db()
218
240
  FileTest.exists?('.issues') || Dir.mkdir('.issues')
219
241
  File.open(@database_file, 'w' ) { |out| YAML.dump(@issues_array, out) }
220
242
  end
221
243
 
222
-
223
- #----------------------------------------------------------------------------------------------------------------------#
244
+ #-------------------------------------------------------------------------------------------
224
245
 
225
246
  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 }
247
+ issue_types = %w{bug improvement task feature}
248
+ issue_type = opts[:type] ? opts[:type].downcase : nil
229
249
 
230
250
  issue_type && (return issue_type)
231
251
 
232
252
  case opts[:title]
233
- when /\b(improve|implement)/i
253
+ when /\b(improve)/i
234
254
  "improvement"
255
+ when /\b(implement)/i
256
+ "feature"
235
257
  when /\b(fix|bug|crash)/i
236
258
  "bug"
237
259
  else
@@ -239,47 +261,100 @@ class IssuesDb
239
261
  end
240
262
  end
241
263
 
242
-
243
- #----------------------------------------------------------------------------------------------------------------------#
264
+ #-------------------------------------------------------------------------------------------
244
265
 
245
266
  def create_issue(opts)
246
267
  type = determine_issue_type(opts)
247
268
  new_issue = Issue.createNewIssue(opts[:title], type)
269
+ new_issue.milestone = opts[:milestone]
270
+ new_issue.estimate = opts[:estimate]
248
271
  @issues_array << new_issue
249
272
  save_db()
250
273
  puts "Created issue #{new_issue.short_id} #{new_issue.title}"
251
274
  end
252
275
 
276
+ #-------------------------------------------------------------------------------------------
277
+
278
+ def select_issues_with_opts(opts)
279
+ did_select_issue_types = opts[:type]
280
+ status_regex = opts[:all] ? /^(open|resolved|duplicate|wontfix)/ : /^open$/
281
+ did_select_milestone = opts[:milestone] != nil
282
+ milestone = opts[:milestone]
283
+
284
+ return @issues_array.select do |issue|
285
+ (status_regex =~ issue.status) \
286
+ && (issue.type == opts[:type] || !did_select_issue_types) \
287
+ && (issue.milestone == opts[:milestone] \
288
+ || !did_select_milestone \
289
+ || milestone.end_with?(".") && issue.milestone.start_with?(milestone)\
290
+ || milestone.start_with?(".") && issue.milestone.end_with?(milestone)) \
291
+ && (issue.milestone == nil || opts["no-milestone"] == false)
292
+ end
293
+ end
294
+
295
+ #-------------------------------------------------------------------------------------------
296
+
297
+ def has_open_issues_with_milestone?
298
+ @issues_array.any? { |issue| issue.status == "open" && issue.milestone != nil }
299
+ end
300
+
301
+ #-------------------------------------------------------------------------------------------
302
+
303
+ def has_open_issues_with_estimate?
304
+ @issues_array.any? { |issue| issue.status == "open" && issue.estimate != nil }
305
+ end
306
+
307
+ #-------------------------------------------------------------------------------------------
308
+
309
+ def milestone_column_width(array = @issues_array)
310
+ result = 0
311
+
312
+ array.each { |issue| issue.milestone != nil && result = max(issue.milestone.length, result) }
313
+
314
+ result
315
+ end
316
+
317
+ #-------------------------------------------------------------------------------------------
318
+
319
+ def sort_none(a, b)
320
+ return 0
321
+ end
322
+ #-------------------------------------------------------------------------------------------
323
+
324
+ def sort_milestone(a, b)
325
+ return a.milestone <=> b.milestone
326
+ end
253
327
 
254
- #----------------------------------------------------------------------------------------------------------------------#
328
+ #-------------------------------------------------------------------------------------------
255
329
 
256
330
  def list_issues(opts)
257
331
  if opts[:issue_id]
258
- list_issue(opts[:issue_id])
332
+ list_issue(opts)
333
+ sort_proc = "sort_milestone"
259
334
  else
260
335
  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}
336
+ select_issues_with_opts(opts).\
337
+ sort {|a,b| sort_milestone(a,b)}.\
338
+ each {|issue| puts issue.method(list_proc).call(opts)}
270
339
  end
271
340
  end
272
341
 
342
+ #-------------------------------------------------------------------------------------------
273
343
 
274
- #----------------------------------------------------------------------------------------------------------------------#
275
-
276
- def list_issue(issue_id)
344
+ def list_issue(opts)
345
+ issue_id = opts[:issue_id]
277
346
  issue = select_issue {|i| i.id.start_with?(issue_id) }
278
- issue.format_verbose()
347
+ puts issue.format_verbose(opts)
279
348
  end
280
349
 
350
+ #-------------------------------------------------------------------------------------------
351
+
352
+ def count_issues(opts)
353
+ num_issues = select_issues_with_opts(opts).count
354
+ puts "#{num_issues} issues."
355
+ end
281
356
 
282
- #----------------------------------------------------------------------------------------------------------------------#
357
+ #-------------------------------------------------------------------------------------------
283
358
 
284
359
  def resolve_issues(opts)
285
360
  issue_id = opts[:issue_id]
@@ -293,6 +368,8 @@ class IssuesDb
293
368
  ["resolved", "Resolved"]
294
369
  when "wontfix"
295
370
  ["wontfix", "Won't fix"]
371
+ when "cantreproduce"
372
+ ["cantreproduce", "Can't reproduce"]
296
373
  when "duplicate"
297
374
  ["duplicate(#{duplicate_of_id})", "Duplicate"]
298
375
  end
@@ -300,16 +377,25 @@ class IssuesDb
300
377
  resolved_issue.status = status
301
378
  resolved_issue.log "Changed status to #{status}"
302
379
 
303
- message = "#{message} issue #{resolved_issue.short_id} #{resolved_issue.title}"
380
+ message = "#{message} issue #{resolved_issue.short_id}: #{resolved_issue.title}"
304
381
  puts message
305
382
 
306
-
307
383
  save_db()
308
- opts[:commit] && exec("git commit -a -m \"#{message}\"")
309
- end
310
384
 
385
+ if opts[:commit]
386
+ system "git add .issues"
387
+ system "git commit -m \"#{message}\""
388
+ end
389
+
390
+ if opts[:close_branch] == true
391
+ issue_branch = git_get_current_branch()
392
+ system "git checkout dev"
393
+ system "git merge #{issue_branch}"
394
+ system "git branch -d #{issue_branch}"
395
+ end
396
+ end
311
397
 
312
- #----------------------------------------------------------------------------------------------------------------------#
398
+ #-------------------------------------------------------------------------------------------
313
399
 
314
400
  def delete_issues(opts)
315
401
  delete_issues = []
@@ -339,8 +425,7 @@ class IssuesDb
339
425
 
340
426
  end
341
427
 
342
-
343
- #----------------------------------------------------------------------------------------------------------------------#
428
+ #-------------------------------------------------------------------------------------------
344
429
 
345
430
  def edit_issue(opts)
346
431
  issue_id = opts[:issue_id]
@@ -357,35 +442,123 @@ class IssuesDb
357
442
  did_change_issue && save_db()
358
443
  end
359
444
 
445
+ #-------------------------------------------------------------------------------------------
446
+
447
+ def select_issue_for_estimate?(issue, milestone)
448
+ issue.milestone != nil \
449
+ && issue.status == "open" \
450
+ && (issue.milestone == milestone \
451
+ || milestone.start_with?(".") && issue.milestone.end_with?(milestone) \
452
+ || milestone.end_with?(".") && issue.milestone.start_with?(milestone) )
453
+ end
454
+
455
+ #-------------------------------------------------------------------------------------------
456
+
457
+ def get_milestones_for_pattern(pattern)
458
+ result_hash = {}
459
+ issues_array.each { |i| select_issue_for_estimate?(i, pattern) && result_hash[i.milestone] = true }
460
+ result_hash.keys.sort
461
+ end
462
+
463
+ #-------------------------------------------------------------------------------------------
360
464
 
361
- #----------------------------------------------------------------------------------------------------------------------#
465
+ def estimate(opts)
466
+ milestone = opts[:milestone]
467
+ total = 0.0
468
+
469
+ issues_to_estimate = @issues_array.select { |i| select_issue_for_estimate?(i, milestone) }
470
+ issues_without_estimates = @issues_array.select { |i| select_issue_for_estimate?(i, milestone) && !i.estimate }
471
+
472
+ if issues_to_estimate.empty?
473
+ puts "No open issues found for milestone " << milestone << "!"
474
+ exit
475
+ end
476
+
477
+ if issues_without_estimates.count > 0
478
+ puts PrettyComment.h3("Warning: Found issues without estimates")
479
+ issues_without_estimates.each {|i| puts i.format_list(opts)}
480
+ puts PrettyComment.separator("-")
481
+ puts
482
+ end
483
+
484
+ issues_to_estimate.each { |i| (i.estimate && total += i.estimate) }
485
+
486
+ milestones = get_milestones_for_pattern(milestone)
362
487
 
363
- def set_type(opts)
364
- new_type = opts[:new_type]
488
+ result_str = "Milestone" << (milestones.length > 1 ? "s" : "")
489
+
490
+ puts result_str << " #{milestones.join(", ")} will take #{total} man-hours to complete."
491
+ puts
492
+ end
493
+
494
+ #-------------------------------------------------------------------------------------------
495
+
496
+ def update(opts)
497
+ new_type = opts[:type]
498
+ new_milestone = opts[:milestone]
499
+ new_estimate = opts[:estimate]
500
+
365
501
  opts[:issue_ids].each do |issue_id|
502
+ did_update = false
366
503
  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}."
504
+
505
+ if new_type
506
+ if issue.type != new_type
507
+ issue.type = new_type
508
+ issue.log("Changed type to #{issue.type}")
509
+ did_update = true
510
+ else
511
+ puts "Issue #{issue.short_id} already of type #{issue.type}."
512
+ end
513
+ end
514
+
515
+ if new_milestone
516
+ if issue.milestone != new_milestone
517
+ issue.milestone = new_milestone
518
+ issue.log("Changed milestone to #{issue.milestone}")
519
+ did_update = true
520
+ else
521
+ puts "Issue #{issue.short_id} already in milestone #{issue.milestone}."
522
+ end
523
+ end
524
+
525
+ if new_estimate
526
+ if issue.estimate != new_estimate
527
+ issue.estimate = new_estimate
528
+ issue.log("Changed estimate to #{issue.estimate}")
529
+ did_update = true
530
+ else
531
+ puts "Issue #{issue.short_id} already has estimate #{issue.estimate}h."
532
+ end
533
+ end
534
+
535
+ if did_update == true
536
+ puts issue.format_list(opts)
537
+ save_db()
373
538
  end
374
539
  end
375
-
376
- save_db()
377
540
  end
378
541
 
542
+ #-------------------------------------------------------------------------------------------
379
543
 
380
- #----------------------------------------------------------------------------------------------------------------------#
544
+ def start_branch(opts)
545
+ git_get_current_branch() == "dev" || Trollop::die("Must be on dev branch to start new issue branch.")
546
+
547
+ issue_id = opts[:issue_id]
548
+ issue = select_issue{|i| i.id.start_with?(issue_id) && i.status == "open"}
549
+
550
+ system("git checkout -b #{issue.type}/#{issue.short_id}")
551
+ end
552
+
553
+ #-------------------------------------------------------------------------------------------
381
554
 
382
555
 
383
556
  end
384
557
 
385
558
 
386
- #======================================================================================================================#
559
+ #===========================================================================================
387
560
  # Command Line Parsing
388
- #======================================================================================================================#
561
+ #===========================================================================================
389
562
 
390
563
  def get_issue_ids(num_ids, usage)
391
564
  result = []
@@ -404,21 +577,24 @@ def get_issue_ids(num_ids, usage)
404
577
  end
405
578
  end
406
579
 
407
-
408
- #----------------------------------------------------------------------------------------------------------------------#
580
+ #-------------------------------------------------------------------------------------------
409
581
 
410
582
  EXECUTABLE_NAME=File.basename($0)
411
583
  DATABASE_NAME= ".issues/" << EXECUTABLE_NAME << ".yaml"
412
584
 
413
585
  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"}
586
+ "list" => "list issues",
587
+ "count" => "count issues",
588
+ "create" => "create a new issue",
589
+ "resolve" => "set status of issue to \"resolved\"",
590
+ "wontfix" => "set status of issue to \"won't fix\"",
591
+ "cantreproduce" => "set status of issue to \"can't reproduce\"",
592
+ "duplicate" => "mark issue as duplicate of another issue",
593
+ "edit" => "edit an existing issue",
594
+ "delete" => "delete an issue",
595
+ "estimate" => "calculate estimated man-hours for a milestone",
596
+ "update" => "update issue fields.",
597
+ "startbranch" => "start a new git branch with to work on issue"}
422
598
 
423
599
  LeftFieldLength =
424
600
  SUB_COMMANDS.collect { |key, value| key.length }.max
@@ -451,31 +627,33 @@ cmd ||= 'list'
451
627
 
452
628
  cmd_opts = {}
453
629
 
454
- if cmd == 'list'
630
+ if cmd == 'list' || cmd == 'count'
455
631
  cmd_opts =
456
632
  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'
633
+ opt :all, "#{cmd} all issues", :short => 'a'
634
+ opt :newest, "#{cmd} newest issues first" if cmd == 'list'
635
+ opt :oldest, "#{cmd} oldest issues first" if cmd == 'list'
636
+ opt :verbose, "verbose list of issues", :short => 'v' if cmd == 'list'
637
+ opt :type, "#{cmd} issues of given type (bug, improvement, task, feature)", :short => 't', :type => String
638
+ opt :milestone, "#{cmd} issues for given milestone", :short => 'm', :type => String
639
+ opt "no-milestone", "#{cmd} all issues not assigned to any milestone"
464
640
  end
465
641
 
466
- ARGV.count > 0 && cmd_opts[:issue_id] = get_issue_ids(1, "list ID")
642
+ if cmd == 'list'
643
+ ARGV.count > 0 && cmd_opts[:issue_id] = get_issue_ids(1, "list ID")
644
+ end
467
645
 
468
646
  elsif cmd == "create"
469
647
  cmd_opts =
470
648
  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'
649
+ opt :type, "create issue of specific type (e.g. bug, feature, improvement, task)", :short => 't', :type => String
650
+ opt :milestone, "specify the milestone of the new issue", :short => 'm', :type => String
651
+ opt :estimate, "specify the estimated effort", :short => 'e', :type => Float
474
652
  end
475
653
  cmd_opts[:title] = ARGV.shift || Trollop::die( "Please enter a title for the new issue!")
476
654
 
477
655
 
478
- elsif cmd == "resolve" || cmd == "wontfix" || cmd == "duplicate"
656
+ elsif cmd == "resolve" || cmd == "wontfix" || cmd == "duplicate" || cmd == "cantreproduce"
479
657
  cmd_opts =
480
658
  Trollop::options do
481
659
  opt :commit, "do a git commit", :short => 'c'
@@ -483,6 +661,10 @@ elsif cmd == "resolve" || cmd == "wontfix" || cmd == "duplicate"
483
661
 
484
662
  if cmd == "duplicate"
485
663
  cmd_opts[:issue_id], cmd_opts[:duplicate_of_id] = get_issue_ids(2, "duplicate ID(issue) ID(duplicate of)")
664
+ elsif cmd == "resolve" && is_issue_branch()
665
+ cmd_opts[:issue_id] = git_get_current_branch().split('/')[1]
666
+ cmd_opts[:close_branch] = true
667
+ cmd_opts[:commit] = true
486
668
  else
487
669
  cmd_opts[:issue_id] = get_issue_ids(1, "#{cmd} [-c] ID")
488
670
  end
@@ -494,29 +676,56 @@ elsif cmd == "edit"
494
676
  opt :description, "edit the issue description", :short => 'd'
495
677
  end
496
678
  cmd_opts[:issue_id] = get_issue_ids(1, "edit ID")
497
-
498
-
499
- elsif cmd == "set-type"
679
+
680
+ elsif cmd == "estimate"
500
681
  Trollop::options do
501
682
  banner <<-EOL
502
683
  Usage:
503
684
  ------
504
- issues set-type {bug|improvement|task} ID
685
+ issues estimate MILESTONE
686
+
687
+ Options:
688
+ --------
689
+ EOL
690
+ end
691
+
692
+ ARGV.count > 0 || Trollop::die("Usage: issues estmiate MILSTONE")
693
+ cmd_opts[:milestone] = ARGV.shift
694
+
695
+ elsif cmd == "update"
696
+ my_banner = <<-EOL
697
+ Usage:
698
+ ------
699
+ issues update \
700
+ [--type (bug|improvement|task|feature)]\
701
+ [--milestone MILESTONE]\
702
+ [--estimate ESTIMATE]\
703
+ ID{,ID2,...}
505
704
 
506
705
  Options:
507
706
  --------
508
707
  EOL
708
+
709
+ cmd_opts = Trollop::options do
710
+ banner my_banner
711
+ opt :type, "update the issue type", :short => 't', :type => String
712
+ opt :milestone, "update the milestone", :short => 'm', :type => String
713
+ opt :estimate, "update the estimate", :short => 'e', :type => Float
509
714
  end
510
715
 
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")
716
+ cmd_opts[:type] && (%w{bug improvement task feature}.include?(cmd_opts[:type]) || Trollop::die("Please specify one of [bug, improvement, task, freature] as new issue type"))
717
+ cmd_opts[:type] || cmd_opts[:milestone] || cmd_opts[:estimate] || Trollop::die("You must specify one of (--type, --milestone, --estimate)")
718
+
719
+ ARGV.count > 0 && cmd_opts[:issue_ids] = get_issue_ids(-1, "set-type {bug|improvement|task|feature} ID")
515
720
 
516
721
 
517
722
  elsif cmd == "delete"
518
723
  cmd_opts[:issue_ids] = get_issue_ids(-1, "#{cmd} ID")
519
724
 
725
+
726
+ elsif cmd == "startbranch"
727
+ cmd_opts[:issue_id] = get_issue_ids(1, "list ID") || Trollop::die( "Please enter an id for the issue to work on in the new branch!")
728
+
520
729
  else
521
730
  Trollop::die "unknown command #{cmd.inspect}"
522
731
  end
@@ -525,27 +734,35 @@ end
525
734
  cmd_opts[:cmd] = cmd
526
735
 
527
736
 
528
- #======================================================================================================================#
529
- # Main
530
- #======================================================================================================================#
737
+ #===========================================================================================
738
+ # Main
739
+ #===========================================================================================
531
740
 
532
741
  Issues = IssuesDb.new(DATABASE_NAME)
742
+ cmd_opts[:list_estimate] = Issues.has_open_issues_with_estimate?()
743
+ cmd_opts[:milestone_column_width] = Issues.milestone_column_width()
744
+ cmd_opts[:list_milestone] = Issues.has_open_issues_with_milestone?()
533
745
 
534
746
  case cmd
535
747
  when "create"
536
748
  Issues.create_issue(cmd_opts)
537
749
  when "list"
538
750
  Issues.list_issues(cmd_opts)
539
- when "resolve", "wontfix", "duplicate"
751
+ when "count"
752
+ Issues.count_issues(cmd_opts)
753
+ when "resolve", "wontfix", "duplicate", "cantreproduce"
540
754
  Issues.resolve_issues(cmd_opts)
541
755
  when "edit"
542
756
  Issues.edit_issue(cmd_opts)
543
- when "set-type"
544
- Issues.set_type(cmd_opts)
757
+ when "estimate"
758
+ Issues.estimate(cmd_opts)
759
+ when "update"
760
+ Issues.update(cmd_opts)
545
761
  when "delete"
546
762
  Issues.delete_issues(cmd_opts)
763
+ when "startbranch"
764
+ Issues.start_branch(cmd_opts)
547
765
  end
548
766
 
549
-
550
- #======================================================================================================================#
767
+ #===========================================================================================
551
768