taco_it 1.3.1 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/bin/taco CHANGED
@@ -1,749 +1,10 @@
1
1
  #!/bin/env ruby
2
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
-
105
- :priority => { :class => Fixnum, :required => true, :settable => true },
106
-
107
- :description => { :class => String, :required => true, :settable => true },
108
- }
109
-
110
- TEMPLATE =<<-EOT.strip
111
- # Lines beginning with # will be ignored.
112
- Summary : %{summary}
113
- Kind : %{kind}
114
- Status : %{status}
115
- Priority : %{priority}
116
- Owner : %{owner}
117
-
118
- # Everything between the --- lines is Issue Description
119
- ---
120
- %{description}
121
- ---
122
- EOT
123
-
124
- class Invalid < Exception; end
125
- class NotFound < Exception; end
126
-
127
- def initialize(issue={}, changelog=[])
128
- issue = Hash[issue.map { |k, v| [ k.to_sym, v ] }]
129
-
130
- @new = issue[:created_at].nil? && issue[:id].nil?
131
-
132
- issue[:created_at] = Time.now unless issue.include?(:created_at) # intentionally not using ||=
133
- issue[:updated_at] = Time.now unless issue.include?(:updated_at) # intentionally not using ||=
134
- issue[:id] = SecureRandom.uuid.gsub('-', '') unless issue.include?(:id) # intentionally not using ||=
135
-
136
- @changelog = []
137
- @issue = {}
138
-
139
- self.issue = Issue::format_attributes issue
140
-
141
- if changelog.size > 0
142
- @changelog = changelog.map do |thing|
143
- if thing.is_a? Change
144
- thing
145
- else
146
- Change.new thing
147
- end
148
- end
149
- end
150
-
151
- self
152
- end
153
-
154
- def new?
155
- @new
156
- end
157
-
158
- def self.set_allowed_values!(attrs=nil)
159
- if attrs.nil?
160
- SCHEMA_ATTRIBUTES.each { |attr, data| data.delete(:allowed_values) }
161
- else
162
- attrs.each do |attr, values|
163
- raise ArgumentError.new("Unknown Issue attributes: #{attr}") unless SCHEMA_ATTRIBUTES.include? attr
164
-
165
- if SCHEMA_ATTRIBUTES[attr][:class] == Fixnum
166
- values.map!(&:to_i)
167
- end
168
-
169
- SCHEMA_ATTRIBUTES[attr][:allowed_values] = values
170
- end
171
- end
172
- end
173
-
174
- def self.format_attributes(issue_attrs)
175
- attrs = issue_attrs.dup
176
-
177
- attrs.keys.each { |attr| raise ArgumentError.new("Unknown Issue attribute: #{attr}") unless SCHEMA_ATTRIBUTES.include? attr }
178
-
179
- SCHEMA_ATTRIBUTES.each do |attr, cfg|
180
- next unless attrs.include? attr
181
-
182
- case cfg[:class].to_s # can't case on cfg[:class], because class of cfg[:class] is always Class :-)
183
- when 'Time'
184
- unless attrs[attr].is_a?(String) || attrs[attr].is_a?(Time)
185
- raise TypeError.new("#{attr} : expected type #{cfg[:class]}, got type #{attrs[attr].class}")
186
- end
187
-
188
- t = if attrs[attr].is_a?(String)
189
- begin
190
- Time.parse(attrs[attr])
191
- rescue ArgumentError => e
192
- raise TypeError.new(e.to_s)
193
- end
194
- else
195
- attrs[attr]
196
- end
197
- attrs[attr] = timescrub(t)
198
- when 'String'
199
- unless attrs[attr].is_a?(String)
200
- raise TypeError.new("#{attr} : expected type #{cfg[:class]}, got type #{attrs[attr].class}")
201
- end
202
-
203
- attrs[attr] && attrs[attr].strip!
204
- when 'Fixnum'
205
- unless attrs[attr].is_a?(Fixnum) || attrs[attr].is_a?(String)
206
- raise TypeError.new("#{attr} : expected type #{cfg[:class]}, got type #{attrs[attr].class}")
207
- end
208
-
209
- if attrs[attr].is_a?(String)
210
- i = attrs[attr].to_i
211
- raise TypeError.new("#{attr} : expected type #{cfg[:class]}, got type #{attrs[attr].class}") unless i.to_s == attrs[attr]
212
- attrs[attr] = i
213
- end
214
- end
215
- end
216
-
217
- attrs
218
- end
219
-
220
- def <=>(other)
221
- if SCHEMA_ATTRIBUTES.all? { |attr, cfg| self.send(attr) == other.send(attr) }
222
- r = 0
223
- else
224
- if self.created_at == other.created_at
225
- r = self.id <=> other.id
226
- else
227
- r = self.created_at <=> other.created_at
228
- end
229
-
230
- # this clause should not return 0, we've already established inequality
231
- #
232
- r = -1 if r == 0
233
- end
234
-
235
- r
236
- end
237
-
238
- def inspect
239
- fields = SCHEMA_ATTRIBUTES.map do |attr, cfg|
240
- "@#{attr}=#{self.send(attr).inspect}"
241
- end.join ', '
242
-
243
- "#<#{self.class}:0x%016x %s>" % [ object_id, fields ]
244
- end
245
-
246
- def method_missing(method, *args, &block)
247
- method_str = method.to_s
248
- attr = method_str.gsub(/=$/, '').to_sym
249
-
250
- if data = SCHEMA_ATTRIBUTES[attr]
251
- if method_str[-1] == '='
252
- raise NoMethodError unless data[:settable]
253
- self.issue = Issue::format_attributes(@issue.merge( { attr => args.first } ) )
254
- @issue[:updated_at] = timescrub Time.now
255
- else
256
- @issue[attr]
257
- end
258
- else
259
- super
260
- end
261
- end
262
-
263
- def respond_to?(method)
264
- method_str = method.to_s
265
- attr = method_str.gsub(/=$/, '').to_sym
266
-
267
- if data = SCHEMA_ATTRIBUTES[attr]
268
- return method_str[-1] != '=' || data[:settable]
269
- end
270
-
271
- super
272
- end
273
-
274
- def to_s(opts={})
275
- text = <<-EOT.strip
276
- ID : #{id}
277
- Created At : #{date(created_at)}
278
- Updated At : #{date(updated_at)}
279
-
280
- Summary : #{summary}
281
- Kind : #{kind}
282
- Status : #{status}
283
- Priority : #{priority}
284
- Owner : #{owner}
285
-
286
- ---
287
- #{description}
288
- EOT
289
-
290
- if opts[:changelog]
291
- changelog_str = changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")
292
- text << %Q|\n---\n\n#{changelog_str}|
293
- end
294
-
295
- text
296
- end
297
-
298
- def to_json(state=nil)
299
- valid? :raise => true
300
- hash = { :issue => @issue, :changelog => changelog }
301
- JSON.pretty_generate(hash)
302
- end
303
-
304
- def to_template
305
- if new?
306
- header = "# New Issue\n#"
307
- body = TEMPLATE
308
- footer = ""
309
- else
310
- header =<<-EOT
311
- # Edit Issue
312
- #
313
- # ID : #{id}
314
- # Created At : #{created_at}
315
- # Updated At : #{updated_at}
316
- #
317
- EOT
318
- body = TEMPLATE % @issue
319
-
320
- footer =<<-EOT
321
- # ChangeLog
322
- #
323
- #{changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")}
324
- EOT
325
- end
326
-
327
- (header + "\n" + body + "\n\n" + footer).strip
328
- end
329
-
330
- def self.from_json(the_json)
331
- begin
332
- hash = JSON.parse(the_json)
333
- rescue JSON::ParserError => e
334
- raise Issue::Invalid.new(e.to_s)
335
- end
336
-
337
- Issue.new(hash['issue'], hash['changelog'])
338
- end
339
-
340
- def self.from_template(text)
341
- issue = { :description => '' }
342
- reading_description = false
343
-
344
- text.lines.each_with_index do |line, index|
345
- next if line =~ /^#/ || (!reading_description && line =~ /^\s*$/)
346
-
347
- if line =~ /^---$/
348
- # FIXME: this means that there can be multiple description blocks in the template!
349
- #
350
- reading_description = !reading_description
351
- next
352
- end
353
-
354
- if !reading_description && line =~ /^(\w+)\s*:\s*(.*)$/
355
- key, value = $1.downcase.to_sym, $2.strip
356
-
357
- if SCHEMA_ATTRIBUTES.include?(key) && SCHEMA_ATTRIBUTES[key][:settable]
358
- issue[key] = value
359
- else
360
- raise ArgumentError.new("Unknown Issue attribute: #{key} on line #{index+1}") unless SCHEMA_ATTRIBUTES.include?(key)
361
- raise ArgumentError.new("Cannot set write-protected Issue attribute: #{key} on line #{index+1}")
362
- end
363
- elsif reading_description
364
- issue[:description] += line
365
- else
366
- raise ArgumentError.new("Cannot parse line #{index+1}")
367
- end
368
- end
369
-
370
- Issue.new(issue)
371
- end
372
-
373
- def update_from_template!(text)
374
- new_issue = Issue.from_template(text)
375
-
376
- attrs = SCHEMA_ATTRIBUTES.map do |attr, data|
377
- if data[:settable]
378
- [ attr, new_issue.send(attr) ]
379
- else
380
- [ attr, @issue[attr] ]
381
- end
382
- end
383
-
384
- self.issue = Issue::format_attributes(Hash[attrs])
385
- @issue[:updated_at] = timescrub Time.now
386
-
387
- self
388
- end
389
-
390
- def valid?(opts={})
391
- begin
392
- raise Issue::Invalid.new("id is nil") unless id
393
-
394
- SCHEMA_ATTRIBUTES.each do |attr, cfg|
395
- raise Issue::Invalid.new("Missing required attribute: #{attr}") if cfg[:required] && @issue[attr].nil?
396
- end
397
-
398
- @issue.each do |attr, value|
399
- unless @issue[attr].is_a?(SCHEMA_ATTRIBUTES[attr][:class])
400
- raise Issue::Invalid.new("Wrong type: #{attr} (expected #{SCHEMA_ATTRIBUTES[attr][:class]}, got #{@issue[attr.class]})")
401
- end
402
-
403
- if allowed_values = SCHEMA_ATTRIBUTES[attr][:allowed_values]
404
- unless allowed_values.include? @issue[attr]
405
- raise Issue::Invalid.new("#{@issue[attr]} is not an allowed value for #{attr.capitalize}")
406
- end
407
- end
408
-
409
- if SCHEMA_ATTRIBUTES[attr][:class] == String && @issue[attr] =~ /\A\s*\Z/
410
- raise Issue::Invalid.new("Empty string is not allowed for #{attr}")
411
- end
412
- end
413
- rescue Issue::Invalid => e
414
- return false unless opts[:raise]
415
- raise e
416
- end
417
-
418
- true
419
- end
420
-
421
- private
422
- def issue=(new_issue)
423
- new_issue.each do |attr, value|
424
- if SCHEMA_ATTRIBUTES[attr][:settable] && @issue[attr] != new_issue[attr]
425
- @changelog << Change.new(:attribute => attr, :old_value => @issue[attr], :new_value => new_issue[attr])
426
- end
427
- end
428
-
429
- @issue = new_issue
430
- end
431
- end
432
-
433
- class Taco
434
- HOME_DIR = '.taco'
435
-
436
- attr_accessor :home
437
-
438
- class NotFound < Exception; end
439
- class Ambiguous < Exception; end
440
-
441
- def initialize(root_path=nil)
442
- @home = File.join(root_path || Dir.getwd, HOME_DIR)
443
- end
444
-
445
- def init!
446
- raise IOError.new("Could not create #{@home}\nDirectory already exists.") if File.exists?(@home)
447
-
448
- FileUtils.mkdir_p(@home)
449
-
450
- "Initialized #{@home}"
451
- end
452
-
453
- def write!(issue_or_issues)
454
- issues = issue_or_issues.is_a?(Array) ? issue_or_issues : [ issue_or_issues ]
455
-
456
- issues.each do |issue|
457
- the_json = issue.to_json # do this first so we don't bother the filesystem if the issue is invalid
458
- open(File.join(@home, issue.id), 'w') { |f| f.write(the_json) }
459
- end
460
-
461
- issue_or_issues
462
- end
463
-
464
- def read(issue_id)
465
- issue_path = File.join(@home, issue_id)
466
-
467
- unless File.exist? issue_path
468
- entries = Dir[File.join(@home, "*#{issue_id}*")]
469
-
470
- raise NotFound.new("Issue not found.") unless entries.size > 0
471
- unless entries.size == 1
472
- issue_list = entries.map do |entry|
473
- issue = read(File.basename(entry))
474
- "#{issue.id} : #{issue.summary}"
475
- end
476
- raise Ambiguous.new("Found several matching issues:\n%s" % issue_list.join("\n"))
477
- end
478
-
479
- issue_path = entries[0]
480
- issue_id = File.basename entries[0]
481
- end
482
-
483
- the_json = open(issue_path) { |f| f.read }
484
-
485
- issue = Issue.from_json the_json
486
-
487
- raise Issue::Invalid.new("Issue ID does not match filename: #{issue.id} != #{issue_id}") unless issue.id == issue_id
488
-
489
- issue
490
- end
491
-
492
- def list(opts={})
493
- filter_match = if opts.fetch(:filters, []).size > 0
494
- conditions = opts[:filters].map do |filter|
495
- attr, val = filter.split(':')
496
- %Q|i.send("#{attr}") == "#{val}"|
497
- end.join ' && '
498
-
499
- # FIXME: eval-ing user input? madness!
500
- eval "Proc.new { |i| #{conditions} }"
501
- else
502
- nil
503
- end
504
-
505
- ids = Dir.glob("#{@home}/*")
506
-
507
- ids.map do |name|
508
- id = File.basename name
509
- issue = Issue.from_json(open(name) { |f| f.read })
510
-
511
- next unless filter_match.nil? || filter_match.call(issue)
512
-
513
- raise Issue::Invalid.new("Issue ID does not match filename: #{issue.id} != #{id}") unless issue.id == id
514
-
515
- short_id = 8.upto(id.size).each do |n|
516
- short_id = id[0...n]
517
- break short_id unless ids.count { |i| i.include? short_id } > 1
518
- end
519
-
520
- if opts[:short_ids]
521
- [ issue, short_id ]
522
- else
523
- issue
524
- end
525
- end.reject(&:nil?).sort_by { |thing| opts[:short_ids] ? thing[0] : thing}
526
- end
527
- end
528
-
529
- class IssueEditor
530
- def initialize(taco, retry_path)
531
- @taco, @retry_path = taco, retry_path
532
- end
533
-
534
- def new_issue!(opts={})
535
- if opts[:from_file]
536
- text = open(opts[:from_file]) { |f| f.read }
537
- else
538
- raise ArgumentError.new("Please define $EDITOR in your environment.") unless ENV['EDITOR']
539
- text = invoke_editor(opts[:template])
540
- end
541
-
542
- write_issue!(Issue.from_template(text), text) if text
543
- end
544
-
545
- def edit_issue!(issue, opts={})
546
- if text = invoke_editor(opts[:template] || issue.to_template)
547
- write_issue!(issue.update_from_template!(text), text)
548
- end
549
- end
550
-
551
- private
552
- def write_issue!(issue, text)
553
- begin
554
- @taco.write! issue
555
- rescue Exception => e
556
- open(@retry_path, 'w') { |f| f.write(text) } if text
557
- raise e
558
- end
559
-
560
- File.unlink @retry_path rescue nil
561
- issue
562
- end
563
-
564
- def invoke_editor(template)
565
- text = nil
566
- file = Tempfile.new('taco')
567
-
568
- begin
569
- file.write(template)
570
- file.close
571
-
572
- cmd = "$EDITOR #{file.path}"
573
- system(cmd)
574
-
575
- open(file.path) do |f|
576
- text = f.read
577
- end
578
- ensure
579
- File.unlink(file.path) rescue nil
580
- end
581
-
582
- text == template ? nil : text
583
- end
584
- end
585
-
586
- class TacoCLI
587
- RC_NAME = '.tacorc'
588
- RC_TEXT =<<-EOT.strip
589
- # Empty lines and lines beginning with # will be ignored.
590
- #
591
- # comma separated list of valid values for Issue fields
592
- #
593
- Kind = Defect, Feature Request
594
- Status = Open, Closed
595
- Priority = 1, 2, 3, 4, 5
596
-
597
- # Default values for Issue fields
598
- #
599
- DefaultKind = Defect
600
- DefaultStatus = Open
601
- DefaultPriority = 3
602
- EOT
603
- RETRY_NAME = '.taco_retry.txt'
604
- INDEX_ERB_NAME = '.index.html.erb'
605
- INDEX_ERB_SRC_PATH = File.realpath(File.join(File.dirname(__FILE__), '../lib/taco/defaults/index.html.erb'))
606
-
607
- class ParseError < Exception; end
608
-
609
- def initialize(taco=nil)
610
- @taco = taco || Taco.new
611
-
612
- @retry_path = File.join(@taco.home, RETRY_NAME)
613
-
614
- @rc_path = File.join(@taco.home, RC_NAME)
615
- @config = parse_rc
616
-
617
- @index_erb_path = File.join(@taco.home, INDEX_ERB_NAME)
618
-
619
- Issue.set_allowed_values! @config[:allowed]
620
- end
621
-
622
- def init!
623
- out = @taco.init!
624
- open(@rc_path, 'w') { |f| f.write(RC_TEXT) }
625
-
626
- FileUtils.copy(INDEX_ERB_SRC_PATH, @index_erb_path)
627
-
628
- out + "\nPlease edit the config file at #{@rc_path}"
629
- end
630
-
631
- def list(args)
632
- the_list = @taco.list(:short_ids => true, :filters => args).map do |issue, short_id|
633
- "#{short_id} : #{issue.priority} : #{issue.summary}"
634
- end
635
- return "Found no issues." unless the_list.size > 0
636
- the_list.join("\n")
637
- end
638
-
639
- def new!(args, opts)
640
- editor_opts = if opts[:retry]
641
- raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
642
- { :template => open(@retry_path) { |f| f.read } }
643
- elsif args.size == 0
644
- { :template => (Issue.new.to_template % @config[:defaults]) }
645
- elsif args.size == 1
646
- { :from_file => args[0] }
647
- end
648
-
649
- if issue = IssueEditor.new(@taco, @retry_path).new_issue!(editor_opts)
650
- "Created Issue #{issue.id}"
651
- else
652
- "Aborted."
653
- end
654
- end
655
-
656
- def show(args, opts)
657
- if opts[:all]
658
- filters = args.select { |arg| arg.include? ':' }
659
- args = @taco.list(:filters => filters).map(&:id)
660
- end
661
-
662
- args.map { |id| @taco.read(id).to_s(opts) }.join("\n\n")
663
- end
664
-
665
- def edit!(args, opts)
666
- ie = IssueEditor.new @taco, @retry_path
667
-
668
- if opts[:retry]
669
- raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
670
- template = open(@retry_path) { |f| f.read }
671
- end
672
-
673
- if issue = ie.edit_issue!(@taco.read(args[0]), :template => template)
674
- "Updated Issue #{issue.id}"
675
- else
676
- "Aborted."
677
- end
678
- end
679
-
680
- def template(opts)
681
- if opts[:defaults]
682
- (Issue::TEMPLATE % @config[:defaults]).strip
683
- else
684
- Issue::TEMPLATE.gsub(/%{.*?}/, '').strip
685
- end
686
- end
687
-
688
- def html
689
- require 'erb'
690
-
691
- issues = @taco.list
692
- ERB.new(open(@index_erb_path) { |f| f.read }).result(binding)
693
- end
694
-
695
- def push(opts)
696
- opts[:message] ||= 'turn and face the strange'
697
- cmd = "git add . && git commit -am '#{opts[:message]}' && git push"
698
- system(cmd)
699
- end
700
-
701
- private
702
- def parse_rc
703
- defaults = Hash[Issue::SCHEMA_ATTRIBUTES.select { |attr, data| data[:settable] }.map { |attr, data| [ attr, nil ] } ]
704
- config = { :defaults => defaults, :allowed => {} }
705
-
706
- def set_attr(hash, what, attr, value, line)
707
- if data = Issue::SCHEMA_ATTRIBUTES[attr]
708
- if data[:settable]
709
- hash[attr] = value
710
- else
711
- raise ParseError.new("Cannot set #{what} for write-protected Issue attribute '#{attr}' on line #{line}")
712
- end
713
- else
714
- raise ParseError.new("Unknown Issue attribute '#{attr}' on line #{line}")
715
- end
716
- end
717
-
718
- if File.exist? @rc_path
719
- open(@rc_path) do |f|
720
- f.readlines.each_with_index do |line, index|
721
- next if line =~ /^#/ || line =~ /^\s*$/
722
-
723
- if line =~ /^Default(\w+)\s+=\s+(\w+)/
724
- attr, value = $1.strip.downcase.to_sym, $2.strip
725
- set_attr(config[:defaults], 'default', attr, value, index+1)
726
- elsif line =~ /^(\w+)\s*=\s*(.*)$/
727
- attr, values = $1.strip.downcase.to_sym, $2.split(',').map(&:strip)
728
- set_attr(config[:allowed], 'allowed values', attr, values, index+1)
729
- else
730
- raise ParseError.new("Unparseable stuff on line #{index+1}")
731
- end
732
- end
733
- end
734
- end
735
-
736
- config
737
- end
738
- end
739
-
740
- # ########
741
- # main
742
- # ########
3
+ require 'taco/cli'
743
4
 
744
5
  begin
745
- cli = TacoCLI.new(Taco.new)
746
- rescue TacoCLI::ParseError => e
6
+ cli = TacoCLI.new
7
+ rescue TacoRc::ParseError => e
747
8
  puts "Parse error while reading .tacorc: #{e}"
748
9
  exit 1
749
10
  end
@@ -751,7 +12,7 @@ end
751
12
  require 'commander/import'
752
13
 
753
14
  program :name, 'taco'
754
- program :version, '1.3.1'
15
+ program :version, '1.4.0'
755
16
  program :description, 'simple command line issue tracking'
756
17
 
757
18
  command :init do |c|