taco_it 1.3.1 → 1.4.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.
data/lib/taco.rb CHANGED
@@ -1,922 +1,4 @@
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
-
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
- # ########
743
-
744
- begin
745
- cli = TacoCLI.new(Taco.new)
746
- rescue TacoCLI::ParseError => e
747
- puts "Parse error while reading .tacorc: #{e}"
748
- exit 1
749
- end
750
-
751
- require 'commander/import'
752
-
753
- program :name, 'taco'
754
- program :version, '1.3.1'
755
- program :description, 'simple command line issue tracking'
756
-
757
- command :init do |c|
758
- c.syntax = 'taco init'
759
- c.summary = 'initialize a taco repo in the current directory'
760
- c.description = 'Initialize a taco Issue repository in the current working directory'
761
- c.action do |args, options|
762
- begin
763
- # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
764
- raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") unless args.size == 0
765
- puts cli.init!
766
- rescue Exception => e
767
- puts "Error: #{e}"
768
- exit 1
769
- end
770
- end
771
- end
772
-
773
- command :list do |c|
774
- c.syntax = 'taco list'
775
- c.summary = 'list all issues in the repository'
776
- c.description = 'List all taco Issues in the current repository'
777
-
778
- c.action do |args, options|
779
- begin
780
- # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
781
- unless args.all? { |arg| arg =~ /\w+:\w+/ }
782
- raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}")
783
- end
784
- puts cli.list args
785
- rescue Exception => e
786
- puts "Error: #{e}"
787
- exit 1
788
- end
789
- end
790
- end
791
-
792
- command :new do |c|
793
- c.syntax = 'taco new [path_to_issue_template]'
794
- c.summary = 'create a new Issue'
795
- c.description = "Create a new Issue, interactively or from a template file.\n Interactive mode launches $EDITOR with an Issue template."
796
- c.example 'interactive Issue creation', 'taco new'
797
- c.example 'Issue creation from a file', 'taco new /path/to/template'
798
-
799
- c.option '--retry', nil, 'retry a failed Issue creation'
800
-
801
- c.action do |args, options|
802
- begin
803
- # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
804
- raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") if args.size > 1
805
-
806
- begin
807
- puts cli.new! args, { :retry => options.retry }
808
- rescue Issue::Invalid => e
809
- raise Issue::Invalid.new("#{e.to_s}.\nYou can use the --retry option to correct this error.")
810
- end
811
- rescue Exception => e
812
- puts "Error: #{e}"
813
- exit 1
814
- end
815
- end
816
- end
817
-
818
- command :show do |c|
819
- c.syntax = 'taco show <issue id0..issue idN>'
820
- c.summary = 'display details for one or more Issues'
821
- c.description = 'Display details for one or more Issues'
822
- c.example 'show Issue by id', 'taco show 9f9c52ce1ced4ace878155c3a98cced0'
823
- c.example 'show Issue by unique id fragment', 'taco show ce1ced'
824
- c.example 'show two Issues by unique id fragment', 'taco show ce1ced bc2de4'
825
- c.example 'show Issue with changelog', 'taco show --changelog 9f9c52'
826
- c.example "show all Issues with 'kind' value 'kind2'", 'taco show --all kind:kind2'
827
- c.example "show all Issues with 'kind' value 'kind2' and 'owner' value 'mike'", 'taco show --all kind:kind2 owner:mike'
828
-
829
- c.option '--changelog', nil, 'shows the changelog'
830
- c.option '--all', nil, 'show all Issues'
831
-
832
- c.action do |args, options|
833
- begin
834
- puts cli.show args, { :changelog => options.changelog, :all => options.all }
835
- rescue Exception => e
836
- puts "Error: #{e}"
837
- exit 1
838
- end
839
- end
840
- end
841
-
842
- command :edit do |c|
843
- c.syntax = 'taco edit <issue_id>'
844
- c.summary = 'edit an Issue'
845
- c.description = 'Edit details for an Issue'
846
-
847
- c.option '--retry', nil, 'retry a failed Issue edit'
848
-
849
- c.action do |args, options|
850
- begin
851
- # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
852
- raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") unless args.size == 1
853
-
854
- begin
855
- puts cli.edit! args, { :retry => options.retry }
856
- rescue Issue::Invalid => e
857
- raise Issue::Invalid.new("#{e.to_s}.\nYou can use the --retry option to correct this error.")
858
- end
859
- rescue Exception => e
860
- puts "Error: #{e}"
861
- exit 1
862
- end
863
- end
864
- end
865
-
866
- command :template do |c|
867
- c.syntax = 'taco template'
868
- c.summary = 'print the Issue template on stdout'
869
- c.description = 'Print the Issue template on stdout'
870
-
871
- c.option '--defaults', nil, 'Print the Issue template with default values'
872
-
873
- c.action do |args, options|
874
- begin
875
- # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
876
- raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") unless args.size == 0
877
-
878
- puts cli.template({ :defaults => options.defaults })
879
- rescue Exception => e
880
- puts "Error: #{e}"
881
- exit 1
882
- end
883
- end
884
- end
885
-
886
- command :html do |c|
887
- c.syntax = 'taco html'
888
- c.summary = 'Generate an HTML buglist'
889
- c.description = 'Generate an HTML buglist from index.html.erb'
890
-
891
- c.action do |args, options|
892
- begin
893
- # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
894
- raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") unless args.size == 0
895
-
896
- puts cli.html
897
- rescue Exception => e
898
- puts "Error: #{e}"
899
- exit 1
900
- end
901
- end
902
- end
903
-
904
- command :push do |c|
905
- c.syntax = 'taco push'
906
- c.summary = 'Add, commit, and push all changes to git remote.'
907
- c.description = 'Shortcut for: git add . && git commit -a && git push'
908
-
909
- c.option '--message STRING', String, 'Override the default commit message.'
910
-
911
- c.action do |args, options|
912
- begin
913
- # FIXME: merge this kind of thing into commander: tell it how many arguments we expect.
914
- raise ArgumentError.new("Unexpected arguments: #{args.join(', ')}") unless args.size == 0
915
-
916
- puts cli.push({ :message => options.message })
917
- rescue Exception => e
918
- puts "Error: #{e}"
919
- exit 1
920
- end
921
- end
922
- end
1
+ require 'taco/change.rb'
2
+ require 'taco/schema.rb'
3
+ require 'taco/issue.rb'
4
+ require 'taco/taco.rb'