taco_it 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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