taco_it 1.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/taco +834 -0
  2. data/lib/taco.rb +834 -0
  3. metadata +64 -0
data/bin/taco ADDED
@@ -0,0 +1,834 @@
1
+ #!/bin/env ruby
2
+
3
+ require 'json'
4
+ require 'digest'
5
+ require 'tempfile'
6
+ require 'fileutils'
7
+ require 'securerandom'
8
+ require 'time'
9
+
10
+ def timescrub(t)
11
+ # Time objects have sub-second precision. Unfortunately, this precision is lost when we serialize. What this means
12
+ # is that the following code will fail, most unexpectedly:
13
+ #
14
+ # i0 = Issue.new some_attributes
15
+ # i1 = Issue.from_json(i0.to_json)
16
+ # i0.created_at == i1.created_at # this will be false!
17
+ #
18
+ Time.new t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset
19
+ end
20
+
21
+ # it's rude to pollute the global namespace, but here we go.
22
+ #
23
+ def date(t)
24
+ t.strftime "%Y/%m/%d %H:%M:%S"
25
+ end
26
+
27
+ class Change
28
+ class Invalid < Exception; end
29
+
30
+ attr_reader :created_at
31
+ attr_accessor :attribute
32
+ attr_accessor :old_value
33
+ attr_accessor :new_value
34
+
35
+ def initialize(args={})
36
+ args.each do |attr, value|
37
+ raise ArgumentError.new("Unknown attribute #{attr}") unless self.respond_to?(attr)
38
+
39
+ case attr.to_sym
40
+ when :created_at
41
+ value = Time.parse(value) unless value.is_a?(Time)
42
+ when :attribute
43
+ value = value.to_sym
44
+ end
45
+
46
+ instance_variable_set("@#{attr.to_s}", value)
47
+ end
48
+
49
+ @created_at = Time.parse(@created_at) if @created_at.is_a?(String)
50
+ @created_at = timescrub(@created_at || Time.now)
51
+
52
+ self
53
+ end
54
+
55
+ def self.from_json(the_json)
56
+ begin
57
+ hash = JSON.parse(the_json)
58
+ rescue JSON::ParserError => e
59
+ raise Change::Invalid.new(e.to_s)
60
+ end
61
+
62
+ Change.new(hash)
63
+ end
64
+
65
+ def valid?(opts={})
66
+ # old_value is optional!
67
+ #
68
+ valid = created_at && attribute && new_value
69
+ raise Invalid if opts[:raise] && !valid
70
+ valid
71
+ end
72
+
73
+ def to_json(state=nil)
74
+ valid? :raise => true
75
+ hash = { :created_at => created_at, :attribute => attribute, :old_value => old_value, :new_value => new_value }
76
+ JSON.pretty_generate(hash)
77
+ end
78
+
79
+ def to_s(opts={})
80
+ if opts[:simple]
81
+ "#{attribute} : #{old_value} => #{new_value}"
82
+ else
83
+ fields = [ date(created_at), attribute, old_value || '[nil]', new_value ]
84
+ "%10s : %12s : %s => %s" % fields
85
+ end
86
+ end
87
+ end
88
+
89
+
90
+ class Issue
91
+ include Comparable
92
+
93
+ attr_reader :changelog
94
+
95
+ SCHEMA_ATTRIBUTES = {
96
+ :id => { :class => String, :required => true, :settable => false },
97
+ :created_at => { :class => Time, :required => true, :settable => false },
98
+ :updated_at => { :class => Time, :required => true, :settable => false },
99
+
100
+ :summary => { :class => String, :required => true, :settable => true },
101
+ :kind => { :class => String, :required => true, :settable => true },
102
+ :status => { :class => String, :required => true, :settable => true },
103
+ :owner => { :class => String, :required => true, :settable => true },
104
+ :description => { :class => String, :required => true, :settable => true },
105
+ }
106
+
107
+ TEMPLATE =<<-EOT.strip
108
+ # Lines beginning with # will be ignored.
109
+ Summary : %{summary}
110
+ Kind : %{kind}
111
+ Status : %{status}
112
+ Owner : %{owner}
113
+
114
+ # Everything between the --- lines is Issue Description
115
+ ---
116
+ %{description}
117
+ ---
118
+ EOT
119
+
120
+ class Invalid < Exception; end
121
+ class NotFound < Exception; end
122
+
123
+ def initialize(issue={}, changelog=[])
124
+ issue = Hash[issue.map { |k, v| [ k.to_sym, v ] }]
125
+
126
+ @new = issue[:created_at].nil? && issue[:id].nil?
127
+
128
+ issue[:created_at] = Time.now unless issue.include?(:created_at) # intentionally not using ||=
129
+ issue[:updated_at] = Time.now unless issue.include?(:updated_at) # intentionally not using ||=
130
+ issue[:id] = SecureRandom.uuid.gsub('-', '') unless issue.include?(:id) # intentionally not using ||=
131
+
132
+ @changelog = []
133
+ @issue = {}
134
+
135
+ self.issue = Issue::format_attributes issue
136
+
137
+ if changelog.size > 0
138
+ @changelog = changelog.map do |thing|
139
+ if thing.is_a? Change
140
+ thing
141
+ else
142
+ Change.new thing
143
+ end
144
+ end
145
+ end
146
+
147
+ self
148
+ end
149
+
150
+ def new?
151
+ @new
152
+ end
153
+
154
+ def self.set_allowed_values!(attrs=nil)
155
+ if attrs.nil?
156
+ SCHEMA_ATTRIBUTES.each { |attr, data| data.delete(:allowed_values) }
157
+ else
158
+ attrs.each do |attr, values|
159
+ raise ArgumentError.new("Unknown Issue attributes: #{attr}") unless SCHEMA_ATTRIBUTES.include? attr
160
+
161
+ SCHEMA_ATTRIBUTES[attr][:allowed_values] = values
162
+ end
163
+ end
164
+ end
165
+
166
+ def self.format_attributes(issue_attrs)
167
+ attrs = issue_attrs.dup
168
+
169
+ attrs.keys.each { |attr| raise ArgumentError.new("Unknown Issue attribute: #{attr}") unless SCHEMA_ATTRIBUTES.include? attr }
170
+
171
+ SCHEMA_ATTRIBUTES.each do |attr, cfg|
172
+ next unless attrs.include? attr
173
+
174
+ case cfg[:class].to_s # can't case on cfg[:class], because class of cfg[:class] is always Class :-)
175
+ when 'Time'
176
+ unless attrs[attr].is_a?(String) || attrs[attr].is_a?(Time)
177
+ raise ArgumentError.new("#{attr} : expected type #{cfg[:class]}, got type #{attrs[attr].class}")
178
+ end
179
+
180
+ t = attrs[attr].is_a?(String) ? Time.parse(attrs[attr]) : attrs[attr]
181
+ attrs[attr] = timescrub(t)
182
+ when 'String'
183
+ unless attrs[attr].is_a?(String)
184
+ raise ArgumentError.new("#{attr} : expected type #{cfg[:class]}, got type #{attrs[attr].class}")
185
+ end
186
+
187
+ attrs[attr] && attrs[attr].strip!
188
+ end
189
+ end
190
+
191
+ attrs
192
+ end
193
+
194
+ def <=>(other)
195
+ if SCHEMA_ATTRIBUTES.all? { |attr, cfg| self.send(attr) == other.send(attr) }
196
+ r = 0
197
+ else
198
+ if self.created_at == other.created_at
199
+ r = self.id <=> other.id
200
+ else
201
+ r = self.created_at <=> other.created_at
202
+ end
203
+
204
+ # this clause should not return 0, we've already established inequality
205
+ #
206
+ r = -1 if r == 0
207
+ end
208
+
209
+ r
210
+ end
211
+
212
+ def inspect
213
+ fields = SCHEMA_ATTRIBUTES.map do |attr, cfg|
214
+ "@#{attr}=#{self.send(attr).inspect}"
215
+ end.join ', '
216
+
217
+ "#<#{self.class}:0x%016x %s>" % [ object_id, fields ]
218
+ end
219
+
220
+ def method_missing(method, *args, &block)
221
+ method_str = method.to_s
222
+ attr = method_str.gsub(/=$/, '').to_sym
223
+
224
+ if data = SCHEMA_ATTRIBUTES[attr]
225
+ if method_str[-1] == '='
226
+ raise NoMethodError unless data[:settable]
227
+ self.issue = Issue::format_attributes(@issue.merge( { attr => args.first } ) )
228
+ @issue[:updated_at] = timescrub Time.now
229
+ else
230
+ @issue[attr]
231
+ end
232
+ else
233
+ super
234
+ end
235
+ end
236
+
237
+ def respond_to?(method)
238
+ method_str = method.to_s
239
+ attr = method_str.gsub(/=$/, '').to_sym
240
+
241
+ if data = SCHEMA_ATTRIBUTES[attr]
242
+ return method_str[-1] != '=' || data[:settable]
243
+ end
244
+
245
+ super
246
+ end
247
+
248
+ def to_s(opts={})
249
+ text = <<-EOT.strip
250
+ ID : #{id}
251
+ Created At : #{date(created_at)}
252
+ Updated At : #{date(updated_at)}
253
+
254
+ Summary : #{summary}
255
+ Kind : #{kind}
256
+ Status : #{status}
257
+ Owner : #{owner}
258
+
259
+ ---
260
+ #{description}
261
+ EOT
262
+
263
+ if opts[:changelog]
264
+ changelog_str = changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")
265
+ text << %Q|\n---\n\n#{changelog_str}|
266
+ end
267
+
268
+ text
269
+ end
270
+
271
+ def to_json(state=nil)
272
+ valid? :raise => true
273
+ hash = { :issue => @issue, :changelog => changelog }
274
+ JSON.pretty_generate(hash)
275
+ end
276
+
277
+ def to_template
278
+ if new?
279
+ header = "# New Issue\n#"
280
+ body = TEMPLATE
281
+ footer = ""
282
+ else
283
+ header =<<-EOT
284
+ # Edit Issue
285
+ #
286
+ # ID : #{id}
287
+ # Created At : #{created_at}
288
+ # Updated At : #{updated_at}
289
+ #
290
+ EOT
291
+ body = TEMPLATE % @issue
292
+
293
+ footer =<<-EOT
294
+ # ChangeLog
295
+ #
296
+ #{changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")}
297
+ EOT
298
+ end
299
+
300
+ (header + "\n" + body + "\n\n" + footer).strip
301
+ end
302
+
303
+ def self.from_json(the_json)
304
+ begin
305
+ hash = JSON.parse(the_json)
306
+ rescue JSON::ParserError => e
307
+ raise Issue::Invalid.new(e.to_s)
308
+ end
309
+
310
+ Issue.new(hash['issue'], hash['changelog'])
311
+ end
312
+
313
+ def self.from_template(text)
314
+ issue = { :description => '' }
315
+ reading_description = false
316
+
317
+ text.lines.each_with_index do |line, index|
318
+ next if line =~ /^#/ || (!reading_description && line =~ /^\s*$/)
319
+
320
+ if line =~ /^---$/
321
+ # FIXME: this means that there can be multiple description blocks in the template!
322
+ #
323
+ reading_description = !reading_description
324
+ next
325
+ end
326
+
327
+ if !reading_description && line =~ /^(\w+)\s*:\s*(.*)$/
328
+ key, value = $1.downcase.to_sym, $2.strip
329
+
330
+ if SCHEMA_ATTRIBUTES.include?(key) && SCHEMA_ATTRIBUTES[key][:settable]
331
+ issue[key] = value
332
+ else
333
+ raise ArgumentError.new("Unknown Issue attribute: #{key} on line #{index+1}") unless SCHEMA_ATTRIBUTES.include?(key)
334
+ raise ArgumentError.new("Cannot set write-protected Issue attribute: #{key} on line #{index+1}")
335
+ end
336
+ elsif reading_description
337
+ issue[:description] += line
338
+ else
339
+ raise ArgumentError.new("Cannot parse line #{index+1}")
340
+ end
341
+ end
342
+
343
+ Issue.new(issue)
344
+ end
345
+
346
+ def update_from_template!(text)
347
+ new_issue = Issue.from_template(text)
348
+
349
+ attrs = SCHEMA_ATTRIBUTES.map do |attr, data|
350
+ if data[:settable]
351
+ [ attr, new_issue.send(attr) ]
352
+ else
353
+ [ attr, @issue[attr] ]
354
+ end
355
+ end
356
+
357
+ self.issue = Issue::format_attributes(Hash[attrs])
358
+ @issue[:updated_at] = timescrub Time.now
359
+
360
+ self
361
+ end
362
+
363
+ def valid?(opts={})
364
+ begin
365
+ raise Issue::Invalid.new("id is nil") unless id
366
+
367
+ SCHEMA_ATTRIBUTES.each do |attr, cfg|
368
+ raise Issue::Invalid.new("Missing required attribute: #{attr}") if cfg[:required] && @issue[attr].nil?
369
+ end
370
+
371
+ @issue.each do |attr, value|
372
+ unless @issue[attr].is_a?(SCHEMA_ATTRIBUTES[attr][:class])
373
+ raise Issue::Invalid.new("Wrong type: #{attr} (expected #{SCHEMA_ATTRIBUTES[attr][:class]}, got #{@issue[attr.class]})")
374
+ end
375
+
376
+ if allowed_values = SCHEMA_ATTRIBUTES[attr][:allowed_values]
377
+ unless allowed_values.include? @issue[attr]
378
+ raise Issue::Invalid.new("#{@issue[attr]} is not an allowed value for #{attr.capitalize}")
379
+ end
380
+ end
381
+
382
+ if SCHEMA_ATTRIBUTES[attr][:class] == String && @issue[attr] =~ /\A\s*\Z/
383
+ raise Issue::Invalid.new("Empty string is not allowed for #{attr}")
384
+ end
385
+ end
386
+ rescue Issue::Invalid
387
+ return false unless opts[:raise]
388
+ raise
389
+ end
390
+
391
+ true
392
+ end
393
+
394
+ private
395
+ def issue=(new_issue)
396
+ new_issue.each do |attr, value|
397
+ if SCHEMA_ATTRIBUTES[attr][:settable] && @issue[attr] != new_issue[attr]
398
+ @changelog << Change.new(:attribute => attr, :old_value => @issue[attr], :new_value => new_issue[attr])
399
+ end
400
+ end
401
+
402
+ @issue = new_issue
403
+ end
404
+ end
405
+
406
+ class Taco
407
+ HOME_DIR = '.taco'
408
+
409
+ attr_accessor :home
410
+
411
+ class NotFound < Exception; end
412
+ class Ambiguous < Exception; end
413
+
414
+ def initialize(root_path=nil)
415
+ @home = File.join(root_path || Dir.getwd, HOME_DIR)
416
+ end
417
+
418
+ def init!
419
+ raise IOError.new("Could not create #{@home}\nDirectory already exists.") if File.exists?(@home)
420
+
421
+ FileUtils.mkdir_p(@home)
422
+
423
+ "Initialized #{@home}"
424
+ end
425
+
426
+ def write!(issue_or_issues)
427
+ issues = issue_or_issues.is_a?(Array) ? issue_or_issues : [ issue_or_issues ]
428
+
429
+ issues.each do |issue|
430
+ the_json = issue.to_json # do this first so we don't bother the filesystem if the issue is invalid
431
+ open(File.join(@home, issue.id), 'w') { |f| f.write(the_json) }
432
+ end
433
+
434
+ issue_or_issues
435
+ end
436
+
437
+ def read(issue_id)
438
+ issue_path = File.join(@home, issue_id)
439
+
440
+ unless File.exist? issue_path
441
+ entries = Dir[File.join(@home, "*#{issue_id}*")]
442
+
443
+ raise NotFound.new("Issue not found.") unless entries.size > 0
444
+ unless entries.size == 1
445
+ issue_list = entries.map do |entry|
446
+ issue = read(File.basename(entry))
447
+ "#{issue.id} : #{issue.summary}"
448
+ end
449
+ raise Ambiguous.new("Found several matching issues:\n%s" % issue_list.join("\n"))
450
+ end
451
+
452
+ issue_path = entries[0]
453
+ issue_id = File.basename entries[0]
454
+ end
455
+
456
+ the_json = open(issue_path) { |f| f.read }
457
+
458
+ issue = Issue.from_json the_json
459
+
460
+ raise Issue::Invalid.new("Issue ID does not match filename: #{issue.id} != #{issue_id}") unless issue.id == issue_id
461
+
462
+ issue
463
+ end
464
+
465
+ def list(opts={})
466
+ filter_match = if opts.fetch(:filters, []).size > 0
467
+ conditions = opts[:filters].map do |filter|
468
+ attr, val = filter.split(':')
469
+ %Q|i.send("#{attr}") == "#{val}"|
470
+ end.join ' && '
471
+
472
+ # FIXME: eval-ing user input? madness!
473
+ eval "Proc.new { |i| #{conditions} }"
474
+ else
475
+ nil
476
+ end
477
+
478
+ ids = Dir.glob("#{@home}/*")
479
+
480
+ ids.map do |name|
481
+ id = File.basename name
482
+ issue = Issue.from_json(open(name) { |f| f.read })
483
+
484
+ next unless filter_match.nil? || filter_match.call(issue)
485
+
486
+ raise Issue::Invalid.new("Issue ID does not match filename: #{issue.id} != #{id}") unless issue.id == id
487
+
488
+ short_id = 8.upto(id.size).each do |n|
489
+ short_id = id[0...n]
490
+ break short_id unless ids.count { |i| i.include? short_id } > 1
491
+ end
492
+
493
+ if opts[:short_ids]
494
+ [ issue, short_id ]
495
+ else
496
+ issue
497
+ end
498
+ end.reject(&:nil?).sort_by { |thing| opts[:short_ids] ? thing[0] : thing}
499
+ end
500
+ end
501
+
502
+ class IssueEditor
503
+ def initialize(taco, retry_path)
504
+ @taco, @retry_path = taco, retry_path
505
+ end
506
+
507
+ def new_issue!(opts={})
508
+ if opts[:from_file]
509
+ text = open(opts[:from_file]) { |f| f.read }
510
+ else
511
+ raise ArgumentError.new("Please define $EDITOR in your environment.") unless ENV['EDITOR']
512
+ text = invoke_editor(opts[:template])
513
+ end
514
+
515
+ write_issue!(Issue.from_template(text), text) if text
516
+ end
517
+
518
+ def edit_issue!(issue, opts={})
519
+ if text = invoke_editor(opts[:template] || issue.to_template)
520
+ write_issue!(issue.update_from_template!(text), text)
521
+ end
522
+ end
523
+
524
+ private
525
+ def write_issue!(issue, text)
526
+ begin
527
+ @taco.write! issue
528
+ rescue Exception => e
529
+ open(@retry_path, 'w') { |f| f.write(text) } if text
530
+ raise e
531
+ end
532
+
533
+ File.unlink @retry_path rescue nil
534
+ issue
535
+ end
536
+
537
+ def invoke_editor(template)
538
+ text = nil
539
+ file = Tempfile.new('taco')
540
+
541
+ begin
542
+ file.write(template)
543
+ file.close
544
+
545
+ cmd = "$EDITOR #{file.path}"
546
+ system(cmd)
547
+
548
+ open(file.path) do |f|
549
+ text = f.read
550
+ end
551
+ ensure
552
+ File.unlink(file.path) rescue nil
553
+ end
554
+
555
+ text == template ? nil : text
556
+ end
557
+ end
558
+
559
+ class TacoCLI
560
+ RC_NAME = '.tacorc'
561
+ RC_TEXT =<<-EOT.strip
562
+ # Empty lines and lines beginning with # will be ignored.
563
+ #
564
+ # comma separated list of valid values for Issue fields
565
+ #
566
+ Kind = Defect, Feature Request
567
+ Status = Open, Closed
568
+
569
+ # Default values for Issue fields
570
+ #
571
+ DefaultKind = Defect
572
+ DefaultStatus = Open
573
+ EOT
574
+ RETRY_NAME = '.taco_retry.txt'
575
+
576
+ class ParseError < Exception; end
577
+
578
+ def initialize(taco=nil)
579
+ @taco = taco || Taco.new
580
+
581
+ @retry_path = File.join(@taco.home, RETRY_NAME)
582
+
583
+ @rc_path = File.join(@taco.home, RC_NAME)
584
+ @config = parse_rc
585
+
586
+ Issue.set_allowed_values! @config[:allowed]
587
+ end
588
+
589
+ def init!
590
+ out = @taco.init!
591
+ open(@rc_path, 'w') { |f| f.write(RC_TEXT) }
592
+ out + "\nPlease edit the config file at #{@rc_path}"
593
+ end
594
+
595
+ def list(args)
596
+ the_list = @taco.list(:short_ids => true, :filters => args).map { |issue, short_id| "#{short_id} : #{issue.summary}" }
597
+ return "Found no issues." unless the_list.size > 0
598
+ the_list.join("\n")
599
+ end
600
+
601
+ def new!(args, opts)
602
+ editor_opts = if opts[:retry]
603
+ raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
604
+ { :template => open(@retry_path) { |f| f.read } }
605
+ elsif args.size == 0
606
+ { :template => (Issue.new.to_template % @config[:defaults]) }
607
+ elsif args.size == 1
608
+ { :from_file => args[0] }
609
+ end
610
+
611
+ if issue = IssueEditor.new(@taco, @retry_path).new_issue!(editor_opts)
612
+ "Created Issue #{issue.id}"
613
+ else
614
+ "Aborted."
615
+ end
616
+ end
617
+
618
+ def show(args, opts)
619
+ if opts[:all]
620
+ filters = args.select { |arg| arg.include? ':' }
621
+ args = @taco.list(:filters => filters).map(&:id)
622
+ end
623
+
624
+ args.map { |id| @taco.read(id).to_s(opts) }.join("\n\n")
625
+ end
626
+
627
+ def edit!(args, opts)
628
+ ie = IssueEditor.new @taco, @retry_path
629
+
630
+ if opts[:retry]
631
+ raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
632
+ template = open(@retry_path) { |f| f.read }
633
+ end
634
+
635
+ if issue = ie.edit_issue!(@taco.read(args[0]), :template => template)
636
+ "Updated Issue #{issue.id}"
637
+ else
638
+ "Aborted."
639
+ end
640
+ end
641
+
642
+ def template(opts)
643
+ if opts[:defaults]
644
+ (Issue::TEMPLATE % @config[:defaults]).strip
645
+ else
646
+ Issue::TEMPLATE.gsub(/%{.*?}/, '').strip
647
+ end
648
+ end
649
+
650
+ private
651
+ def parse_rc
652
+ defaults = Hash[Issue::SCHEMA_ATTRIBUTES.select { |attr, data| data[:settable] }.map { |attr, data| [ attr, nil ] } ]
653
+ config = { :defaults => defaults, :allowed => {} }
654
+
655
+ def set_attr(hash, what, attr, value, line)
656
+ if data = Issue::SCHEMA_ATTRIBUTES[attr]
657
+ if data[:settable]
658
+ hash[attr] = value
659
+ else
660
+ raise ParseError.new("Cannot set #{what} for write-protected Issue attribute '#{attr}' on line #{line}")
661
+ end
662
+ else
663
+ raise ParseError.new("Unknown Issue attribute '#{attr}' on line #{line}")
664
+ end
665
+ end
666
+
667
+ if File.exist? @rc_path
668
+ open(@rc_path) do |f|
669
+ f.readlines.each_with_index do |line, index|
670
+ next if line =~ /^#/ || line =~ /^\s*$/
671
+
672
+ if line =~ /^Default(\w+)\s+=\s+(\w+)/
673
+ attr, value = $1.strip.downcase.to_sym, $2.strip
674
+ set_attr(config[:defaults], 'default', attr, value, index+1)
675
+ elsif line =~ /^(\w+)\s*=\s*(.*)$/
676
+ attr, values = $1.strip.downcase.to_sym, $2.split(',').map(&:strip)
677
+ set_attr(config[:allowed], 'allowed values', attr, values, index+1)
678
+ else
679
+ raise ParseError.new("Unparseable stuff on line #{index+1}")
680
+ end
681
+ end
682
+ end
683
+ end
684
+
685
+ config
686
+ end
687
+ end
688
+
689
+ # ########
690
+ # main
691
+ # ########
692
+
693
+ begin
694
+ cli = TacoCLI.new(Taco.new)
695
+ rescue TacoCLI::ParseError => e
696
+ puts "Parse error while reading .tacorc: #{e}"
697
+ exit 1
698
+ end
699
+
700
+ require 'commander/import'
701
+
702
+ program :name, 'taco'
703
+ program :version, '1.0.0'
704
+ program :description, 'simple command line issue tracking'
705
+
706
+ command :init do |c|
707
+ c.syntax = 'taco init'
708
+ c.summary = 'initialize a taco repo in the current directory'
709
+ c.description = 'Initialize a taco Issue repository in the current working directory'
710
+ c.action do |args, options|
711
+ begin
712
+ # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
713
+ raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") unless args.size == 0
714
+ puts cli.init!
715
+ rescue Exception => e
716
+ puts "Error: #{e}"
717
+ exit 1
718
+ end
719
+ end
720
+ end
721
+
722
+ command :list do |c|
723
+ c.syntax = 'taco list'
724
+ c.summary = 'list all issues in the repository'
725
+ c.description = 'List all taco Issues in the current repository'
726
+
727
+ c.action do |args, options|
728
+ begin
729
+ # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
730
+ unless args.all? { |arg| arg =~ /\w+:\w+/ }
731
+ raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}")
732
+ end
733
+ puts cli.list args
734
+ rescue Exception => e
735
+ puts "Error: #{e}"
736
+ exit 1
737
+ end
738
+ end
739
+ end
740
+
741
+ command :new do |c|
742
+ c.syntax = 'taco new [path_to_issue_template]'
743
+ c.summary = 'create a new Issue'
744
+ c.description = "Create a new Issue, interactively or from a template file.\n Interactive mode launches $EDITOR with an Issue template."
745
+ c.example 'interactive Issue creation', 'taco new'
746
+ c.example 'Issue creation from a file', 'taco new /path/to/template'
747
+
748
+ c.option '--retry', nil, 'retry a failed Issue creation'
749
+
750
+ c.action do |args, options|
751
+ begin
752
+ # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
753
+ raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") if args.size > 1
754
+
755
+ begin
756
+ puts cli.new! args, { :retry => options.retry }
757
+ rescue Issue::Invalid => e
758
+ raise Issue::Invalid.new("#{e.to_s}.\nYou can use the --retry option to correct this error.")
759
+ end
760
+ rescue Exception => e
761
+ puts "Error: #{e}"
762
+ exit 1
763
+ end
764
+ end
765
+ end
766
+
767
+ command :show do |c|
768
+ c.syntax = 'taco show <issue id0..issue idN>'
769
+ c.summary = 'display details for one or more Issues'
770
+ c.description = 'Display details for one or more Issues'
771
+
772
+ c.example 'show Issue by id', 'taco show 9f9c52ce1ced4ace878155c3a98cced0'
773
+ c.example 'show Issue by unique id fragment', 'taco show ce1ced'
774
+ c.example 'show two Issues by unique id fragment', 'taco show ce1ced bc2de4'
775
+ c.example 'show Issue with changelog', 'taco show --changelog 9f9c52'
776
+ c.example "show all Issues with 'kind' value 'kind2'", 'taco show --all kind:kind2'
777
+ c.example "show all Issues with 'kind' value 'kind2' and 'owner' value 'mike'", 'taco show --all kind:kind2 owner:mike'
778
+
779
+ c.option '--changelog', nil, 'shows the changelog'
780
+ c.option '--all', nil, 'show all Issues'
781
+
782
+ c.action do |args, options|
783
+ begin
784
+ puts cli.show args, { :changelog => options.changelog, :all => options.all }
785
+ rescue Exception => e
786
+ puts "Error: #{e}"
787
+ exit 1
788
+ end
789
+ end
790
+ end
791
+
792
+ command :edit do |c|
793
+ c.syntax = 'taco edit <issue_id>'
794
+ c.summary = 'edit an Issue'
795
+ c.description = 'Edit details for an Issue'
796
+
797
+ c.option '--retry', nil, 'retry a failed Issue edit'
798
+
799
+ c.action do |args, options|
800
+ begin
801
+ # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
802
+ raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") unless args.size == 1
803
+
804
+ begin
805
+ puts cli.edit! args, { :retry => options.retry }
806
+ rescue Issue::Invalid => e
807
+ raise Issue::Invalid.new("#{e.to_s}.\nYou can use the --retry option to correct this error.")
808
+ end
809
+ rescue Exception => e
810
+ puts "Error: #{e}"
811
+ exit 1
812
+ end
813
+ end
814
+ end
815
+
816
+ command :template do |c|
817
+ c.syntax = 'taco template'
818
+ c.summary = 'print the Issue template on stdout'
819
+ c.description = 'Print the Issue template on stdout'
820
+
821
+ c.option '--defaults', nil, 'Print the Issue template with default values'
822
+
823
+ c.action do |args, options|
824
+ begin
825
+ # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
826
+ raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") unless args.size == 0
827
+
828
+ puts cli.template({ :defaults => options.defaults })
829
+ rescue Exception => e
830
+ puts "Error: #{e}"
831
+ exit 1
832
+ end
833
+ end
834
+ end