taco_it 1.5.2 → 1.5.3

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.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ M2UwODA4YTVhOTlhZThhOTM1MjIyZjJhY2RhMWVkNmYyY2IyNDQ4Yg==
5
+ data.tar.gz: !binary |-
6
+ ZTljZGJhYTMwNTdmMmI4YzQ3YWU0MWI1ZTY3NjlkYzhkZWZhOWYwYg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ZWQzNTc4NGMwZmZiNDFlOWJjZmJjYWZjN2ZlMjgzOThhOGE0OWJhMDYyNmJm
10
+ NzhkMzEyNDg2ZTgxZDBkOWFlYTIyZjdkZmE0YTU2MDkyNmNkYWU5YjUyZDEw
11
+ YmNmOTI2MTdiN2EyOWIzZjc3MDZjOTMxZjI3OGE5MGNlZTA1ZGI=
12
+ data.tar.gz: !binary |-
13
+ ODQ2Y2I4YTEyNTUwYTczYTUyMzgwMjRlMGMyMzY1NTI5MDc3OTIyNWVlMGY2
14
+ YzY3YTA1ZDQ1MjNjNWMwNzUyY2M3MWNmMDE0MDQyYzcyYWJlMWM5ZGUzODY3
15
+ MzM1NzM2MWNhZjRlYzEyNDk0MjI4MGIxZmM0NTczNTU5NzU5ZGU=
data/bin/taco CHANGED
@@ -12,7 +12,7 @@ end
12
12
  require 'taco/commander/import'
13
13
 
14
14
  program :name, 'taco'
15
- program :version, '1.5.2'
15
+ program :version, '1.5.3'
16
16
  program :description, 'simple command line issue tracking'
17
17
 
18
18
  command :init do |c|
@@ -49,7 +49,7 @@ command :new do |c|
49
49
 
50
50
  c.option '--retry', nil, 'retry a failed Issue creation'
51
51
 
52
- c.arguments lambda { |args| args.size <= 1 }
52
+ c.arguments lambda { |args| true } #lambda { |args| args.size <= 1 }
53
53
 
54
54
  c.action do |args, options|
55
55
  begin
data/lib/taco/change.rb CHANGED
@@ -4,32 +4,32 @@ require 'time'
4
4
  # FIXME: put this in a namespace
5
5
  class Change
6
6
  class Invalid < Exception; end
7
-
7
+
8
8
  attr_reader :created_at
9
9
  attr_accessor :attribute
10
10
  attr_accessor :old_value
11
- attr_accessor :new_value
12
-
11
+ attr_accessor :new_value
12
+
13
13
  def initialize(args={})
14
14
  args.each do |attr, value|
15
15
  raise ArgumentError.new("Unknown attribute #{attr}") unless self.respond_to?(attr)
16
-
16
+
17
17
  case attr.to_sym
18
18
  when :created_at
19
19
  value = Time.parse(value) unless value.is_a?(Time)
20
20
  when :attribute
21
21
  value = value.to_sym
22
22
  end
23
-
23
+
24
24
  instance_variable_set("@#{attr.to_s}", value)
25
25
  end
26
-
26
+
27
27
  @created_at = Time.parse(@created_at) if @created_at.is_a?(String)
28
28
  @created_at = timescrub(@created_at || Time.now)
29
-
29
+
30
30
  self
31
- end
32
-
31
+ end
32
+
33
33
  def self.from_json(the_json)
34
34
  begin
35
35
  hash = JSON.parse(the_json)
@@ -37,9 +37,9 @@ class Change
37
37
  raise Change::Invalid.new(e.to_s)
38
38
  end
39
39
 
40
- Change.new(hash)
40
+ Change.new(hash)
41
41
  end
42
-
42
+
43
43
  def valid?(opts={})
44
44
  # old_value is optional!
45
45
  #
@@ -47,29 +47,29 @@ class Change
47
47
  raise Invalid if opts[:raise] && !valid
48
48
  valid
49
49
  end
50
-
50
+
51
51
  def to_json(state=nil)
52
52
  valid? :raise => true
53
53
  hash = { :created_at => created_at, :attribute => attribute, :old_value => old_value, :new_value => new_value }
54
54
  JSON.pretty_generate(hash)
55
55
  end
56
-
56
+
57
57
  def to_s(opts={})
58
58
  if opts[:simple]
59
59
  "#{attribute} : #{old_value} => #{new_value}"
60
60
  else
61
- fields = [ date(created_at), attribute, old_value || '[nil]', new_value ]
62
- "%10s : %12s : %s => %s" % fields
61
+ fields = [ date(created_at), attribute, old_value || '[nil]', new_value ]
62
+ "%10s : %12s : %s => %s" % fields
63
63
  end
64
64
  end
65
-
65
+
66
66
  private
67
67
  def date(t)
68
68
  t.strftime "%Y/%m/%d %H:%M:%S"
69
69
  end
70
-
70
+
71
71
  def timescrub(t)
72
72
  Time.new t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset
73
73
  end
74
-
74
+
75
75
  end
data/lib/taco/cli.rb CHANGED
@@ -11,16 +11,16 @@ class TacoCLI
11
11
  TACORC_NAME = '.tacorc'
12
12
  TACO_PROFILE_NAME = '.taco_profile'
13
13
  INDEX_ERB_NAME = '.index.html.erb'
14
-
14
+
15
15
  DEFAULT_TACORC_NAME = 'tacorc'
16
16
  DEFAULT_TACO_PROFILE_NAME = 'taco_profile'
17
17
  DEFAULT_INDEX_ERB_NAME = 'index.html.erb'
18
-
18
+
19
19
  DEFAULTS_HOME = File.realpath(File.join(File.dirname(__FILE__), 'defaults/'))
20
-
20
+
21
21
  def initialize
22
22
  @taco = Taco.new
23
-
23
+
24
24
  @retry_path = File.join(@taco.home, RETRY_NAME)
25
25
 
26
26
  @tacorc_path = File.join(@taco.home, TACORC_NAME)
@@ -33,24 +33,24 @@ class TacoCLI
33
33
  rc = TacoRc.new @tacorc_path
34
34
  rc.update_schema! Issue
35
35
  end
36
-
36
+
37
37
  # FIXME: do this elsewhere. pass in an initialized TacoProfile object
38
38
  #
39
39
  if File.exist? @taco_profile_path
40
40
  profile_text = open(@taco_profile_path) { |f| f.read }
41
41
  end
42
-
43
- @profile = TacoProfile.new profile_text
42
+
43
+ @profile = TacoProfile.new profile_text
44
44
  end
45
-
45
+
46
46
  def init!
47
47
  out = @taco.init!
48
48
 
49
- FileUtils.copy(File.join(DEFAULTS_HOME, DEFAULT_TACORC_NAME), @tacorc_path)
50
- FileUtils.copy(File.join(DEFAULTS_HOME, DEFAULT_TACO_PROFILE_NAME), @taco_profile_path)
49
+ FileUtils.copy(File.join(DEFAULTS_HOME, DEFAULT_TACORC_NAME), @tacorc_path)
50
+ FileUtils.copy(File.join(DEFAULTS_HOME, DEFAULT_TACO_PROFILE_NAME), @taco_profile_path)
51
51
 
52
52
  FileUtils.copy(File.join(DEFAULTS_HOME, DEFAULT_INDEX_ERB_NAME), @index_erb_path)
53
-
53
+
54
54
  out + "\nPlease edit the config files at:\n #{@tacorc_path}\n #{@taco_profile_path}"
55
55
  end
56
56
 
@@ -58,40 +58,61 @@ class TacoCLI
58
58
  filters = args.size > 0 ? args : @profile.filters
59
59
 
60
60
  the_list = @taco.list :filters => filters
61
-
61
+
62
62
  if opts[:sort]
63
- attrs = opts[:sort].split(',').map(&:to_s)
63
+ attrs = opts[:sort].split(',').map(&:to_sym)
64
+ attrs.each do |attr|
65
+ # FIXME: don't hardcode :short_id
66
+ raise ArgumentError.new("Unknown Issue attribute for sort: #{attr}") unless attr == :short_id || Issue.schema_attributes.include?(attr)
67
+ end
64
68
  else
65
69
  attrs = @profile.sort_order
66
70
  end
67
-
71
+
68
72
  the_list.sort! do |issue_a, issue_b|
69
73
  order = 0
70
-
74
+
71
75
  attrs.take_while do |attr|
72
76
  order = issue_a.send(attr) <=> issue_b.send(attr)
73
77
  order == 0
74
78
  end
75
-
79
+
76
80
  order
77
81
  end
78
-
82
+
79
83
  the_list.map! do |issue|
80
84
  @profile.columns.map { |col| issue.send(col) }.join(' : ')
81
85
  end
82
-
86
+
83
87
  return "Found no issues." unless the_list.size > 0
84
88
  the_list.join("\n")
85
89
  end
86
-
90
+
87
91
  def new!(args, opts)
88
92
  editor_opts = if opts[:retry]
89
- raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
93
+ raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
90
94
  { :template => open(@retry_path) { |f| f.read } }
91
- elsif args.size == 0
92
- { :template => Issue.new.to_template }
93
- elsif args.size == 1
94
- { :from_file => args[0] }
95
+ else
96
+ file, defaults = nil, {}
97
+
98
+ args.each do |arg|
99
+ if arg.include? ':'
100
+ k, v = arg.split(':', 2)
101
+ defaults[Issue.schema_attr_expand(k)] = v
102
+ elsif file
103
+ raise ArgumentError.new("Multiple filenames given.")
104
+ else
105
+ file = arg
106
+ end
107
+ end
108
+
109
+ if file && defaults.size > 0
110
+ raise ArgumentError.new("Cannot set defaults when creating Issue from file.")
111
+ elsif file
112
+ { :from_file => file, :defaults => defaults }
113
+ else
114
+ { :template => Issue.new.to_template(:defaults => defaults) }
115
+ end
95
116
  end
96
117
 
97
118
  if issue = IssueEditor.new(@taco, @retry_path).new_issue!(editor_opts)
@@ -100,31 +121,31 @@ class TacoCLI
100
121
  "Aborted."
101
122
  end
102
123
  end
103
-
124
+
104
125
  def show(args, opts)
105
126
  if opts[:all]
106
127
  filters = args.select { |arg| arg.include? ':' }
107
128
  args = @taco.list(:filters => filters).map(&:id)
108
129
  end
109
-
130
+
110
131
  args.map { |id| @taco.read(id).to_s(opts) }.join("\n\n")
111
132
  end
112
-
133
+
113
134
  def edit!(args, opts)
114
135
  ie = IssueEditor.new @taco, @retry_path
115
-
136
+
116
137
  if opts[:retry]
117
- raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
138
+ raise ArgumentError.new("No previous Issue edit session was found.") unless File.exist?(@retry_path)
118
139
  template = open(@retry_path) { |f| f.read }
119
140
  end
120
-
141
+
121
142
  if issue = ie.edit_issue!(@taco.read(args[0]), :template => template)
122
143
  "Updated Issue #{issue.id}"
123
144
  else
124
145
  "Aborted."
125
146
  end
126
147
  end
127
-
148
+
128
149
  def template(opts)
129
150
  if opts[:defaults]
130
151
  (Issue::TEMPLATE % Issue.new.to_hash).strip
@@ -132,14 +153,14 @@ class TacoCLI
132
153
  Issue::TEMPLATE.gsub(/%{.*?}/, '').strip
133
154
  end
134
155
  end
135
-
156
+
136
157
  def html
137
158
  require 'erb'
138
-
159
+
139
160
  issues = @taco.list
140
- ERB.new(open(@index_erb_path) { |f| f.read }).result(binding)
161
+ ERB.new(open(@index_erb_path) { |f| f.read }).result(binding)
141
162
  end
142
-
163
+
143
164
  def push(opts)
144
165
  opts[:message] ||= 'turn and face the strange'
145
166
  cmd = "git add . && git commit -am '#{opts[:message]}' && git push"
data/lib/taco/issue.rb CHANGED
@@ -2,25 +2,25 @@ require 'taco/schema'
2
2
  require 'taco/change'
3
3
  require 'securerandom'
4
4
 
5
- class Issue
5
+ class Issue
6
6
  include Comparable
7
7
  include Schema
8
-
8
+
9
9
  alias_method :schema_valid?, :valid?
10
-
10
+
11
11
  attr_reader :changelog
12
12
 
13
13
  schema_attr :id, class: String, settable: false
14
14
  schema_attr :created_at, class: Time, settable: false
15
15
  schema_attr :updated_at, class: Time, settable: false
16
-
16
+
17
17
  schema_attr :summary, class: String, settable: true
18
18
  schema_attr :kind, class: String, settable: true
19
19
  schema_attr :status, class: String, settable: true
20
20
  schema_attr :owner, class: String, settable: true
21
21
  schema_attr :priority, class: Fixnum, settable: true
22
22
  schema_attr :description, class: String, settable: true
23
-
23
+
24
24
  TEMPLATE =<<-EOT.strip
25
25
  # Lines beginning with # will be ignored.
26
26
  Summary : %{summary}
@@ -37,14 +37,14 @@ EOT
37
37
 
38
38
  class Invalid < Exception; end
39
39
  class NotFound < Exception; end
40
-
40
+
41
41
  def initialize(attributes={}, changelog=[])
42
42
  attributes = Hash[attributes.map { |k, v| [ k.to_sym, v ] }]
43
43
 
44
- @new = attributes[:created_at].nil? && attributes[:id].nil?
44
+ @new = attributes[:created_at].nil? && attributes[:id].nil?
45
45
 
46
46
  @changelog = []
47
-
47
+
48
48
  attributes.each do |attr, value|
49
49
  schema_attr = self.class.schema_attributes[attr]
50
50
  raise ArgumentError.new("unknown attribute: #{attr}") unless schema_attr
@@ -52,11 +52,11 @@ EOT
52
52
  self.send "#{attr}=", attributes[attr]
53
53
  end
54
54
  end
55
-
55
+
56
56
  self.id = attributes[:id] || SecureRandom.uuid.gsub('-', '')
57
57
  self.created_at = attributes[:created_at] || Time.now
58
58
  self.updated_at = attributes[:updated_at] || Time.now
59
-
59
+
60
60
  if changelog.size > 0
61
61
  @changelog = changelog.map do |thing|
62
62
  if thing.is_a? Change
@@ -66,21 +66,21 @@ EOT
66
66
  end
67
67
  end
68
68
  end
69
-
69
+
70
70
  self
71
71
  end
72
-
72
+
73
73
  def schema_attribute_change(attribute, old_value, new_value)
74
74
  if self.class.schema_attributes[attribute][:settable]
75
75
  self.updated_at = Time.now
76
76
  @changelog << Change.new(:attribute => attribute, :old_value => old_value, :new_value => new_value)
77
77
  end
78
- end
79
-
78
+ end
79
+
80
80
  def new?
81
81
  @new
82
82
  end
83
-
83
+
84
84
  def <=>(other)
85
85
  if self.class.schema_attributes.all? { |attr, opts| self.send(attr) == other.send(attr) }
86
86
  r = 0
@@ -92,10 +92,10 @@ EOT
92
92
  end
93
93
 
94
94
  # this clause should not return 0, we've already established inequality
95
- #
95
+ #
96
96
  r = -1 if r == 0
97
97
  end
98
-
98
+
99
99
  r
100
100
  end
101
101
 
@@ -103,10 +103,10 @@ EOT
103
103
  fields = self.class.schema_attributes.map do |attr, opts|
104
104
  "@#{attr}=#{self.send(attr).inspect}"
105
105
  end.join ', '
106
-
106
+
107
107
  "#<#{self.class}:0x%016x %s>" % [ object_id, fields ]
108
108
  end
109
-
109
+
110
110
  def to_s(opts={})
111
111
  text = <<-EOT.strip
112
112
  ID : #{id}
@@ -124,30 +124,30 @@ Owner : #{owner}
124
124
  EOT
125
125
 
126
126
  if opts[:changelog]
127
- changelog_str = changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")
127
+ changelog_str = changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")
128
128
  text << %Q|\n---\n\n#{changelog_str}|
129
129
  end
130
-
130
+
131
131
  text
132
132
  end
133
-
133
+
134
134
  def valid?(opts={})
135
135
  valid = schema_valid?
136
136
  error = schema_errors.first
137
137
  raise Invalid.new("attribute #{error.first}: #{error[1].inspect} is not a valid value") if !valid && opts[:raise]
138
138
  valid
139
139
  end
140
-
140
+
141
141
  def to_json(state=nil)
142
142
  valid? :raise => true
143
143
  hash = { :issue => self.to_hash, :changelog => changelog }
144
144
  JSON.pretty_generate(hash)
145
145
  end
146
-
147
- def to_template
146
+
147
+ def to_template(opts={})
148
148
  if new?
149
149
  header = "# New Issue\n#"
150
- body = TEMPLATE % self.to_hash
150
+ body = TEMPLATE % self.to_hash.merge(opts.fetch(:defaults, {}))
151
151
  footer = ""
152
152
  else
153
153
  header =<<-EOT
@@ -159,44 +159,44 @@ EOT
159
159
  #
160
160
  EOT
161
161
  body = TEMPLATE % self.to_hash
162
-
163
- footer =<<-EOT
162
+
163
+ footer =<<-EOT
164
164
  # ChangeLog
165
165
  #
166
- #{changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")}
166
+ #{changelog.map { |c| %Q|# #{c.to_s.strip.gsub(/\n/, "\n# ")}| }.join("\n")}
167
167
  EOT
168
168
  end
169
-
169
+
170
170
  (header + "\n" + body + "\n\n" + footer).strip
171
171
  end
172
-
172
+
173
173
  def self.from_json(the_json)
174
174
  begin
175
175
  hash = JSON.parse(the_json)
176
176
  rescue JSON::ParserError => e
177
177
  raise Issue::Invalid.new(e.to_s)
178
178
  end
179
-
180
- Issue.new(hash['issue'], hash['changelog'])
181
- end
182
-
179
+
180
+ Issue.new(hash['issue'], hash['changelog'])
181
+ end
182
+
183
183
  def self.from_template(text)
184
184
  issue = { :description => '' }
185
185
  reading_description = false
186
-
186
+
187
187
  text.lines.each_with_index do |line, index|
188
188
  next if line =~ /^#/ || (!reading_description && line =~ /^\s*$/)
189
-
189
+
190
190
  if line =~ /^---$/
191
191
  # FIXME: this means that there can be multiple description blocks in the template!
192
192
  #
193
193
  reading_description = !reading_description
194
194
  next
195
195
  end
196
-
196
+
197
197
  if !reading_description && line =~ /^(\w+)\s*:\s*(.*)$/
198
198
  key, value = $1.downcase.to_sym, $2.strip
199
-
199
+
200
200
  if schema_attributes.include?(key) && schema_attributes[key][:settable]
201
201
  issue[key] = value
202
202
  else
@@ -209,13 +209,13 @@ EOT
209
209
  raise ArgumentError.new("Cannot parse line #{index+1}")
210
210
  end
211
211
  end
212
-
212
+
213
213
  Issue.new(issue)
214
214
  end
215
-
215
+
216
216
  def update_from_template!(text)
217
217
  new_issue = Issue.from_template(text)
218
-
218
+
219
219
  attrs = self.class.schema_attributes.map do |attr, opts|
220
220
  if opts[:settable] && self.send(attr) != (new_value = new_issue.send(attr))
221
221
  self.send("#{attr}=", new_value)
@@ -223,17 +223,17 @@ EOT
223
223
  end
224
224
 
225
225
  self
226
- end
227
-
226
+ end
227
+
228
228
  private
229
229
  def date(t)
230
230
  t.strftime "%Y/%m/%d %H:%M:%S"
231
231
  end
232
-
232
+
233
233
  def dup
234
234
  raise NoMethodError.new
235
235
  end
236
-
236
+
237
237
  def clone
238
238
  raise NoMethodError.new
239
239
  end
data/lib/taco/schema.rb CHANGED
@@ -10,20 +10,20 @@ module Schema
10
10
  def self.included(base)
11
11
  base.extend(ClassMethods)
12
12
  end
13
-
13
+
14
14
  def to_hash
15
15
  Hash[self.class.schema_attributes.map do |attr, opts|
16
16
  [ attr, send(attr) ]
17
17
  end]
18
18
  end
19
-
19
+
20
20
  def schema_errors
21
21
  @errors || []
22
22
  end
23
-
23
+
24
24
  def valid?
25
25
  @errors = nil
26
-
26
+
27
27
  self.class.schema_attributes.each do |attr, opts|
28
28
  if opts[:validate].nil?
29
29
  case opts[:class].to_s # can't case on opts[:class], because class of opts[:class] is always Class :-)
@@ -31,55 +31,62 @@ module Schema
31
31
  opts[:validate] = lambda { |v| v !~ /\A\s*\Z/ }
32
32
  end
33
33
  end
34
-
34
+
35
35
  if opts[:validate]
36
36
  value = eval(attr.to_s)
37
-
37
+
38
38
  valid = if opts[:validate].is_a?(Array)
39
39
  opts[:validate].include? value
40
40
  elsif opts[:validate].is_a?(Proc)
41
41
  opts[:validate].call(value)
42
42
  end
43
-
43
+
44
44
  unless valid
45
45
  @errors = [ [ attr, value ] ]
46
46
  return false
47
47
  end
48
48
  end
49
49
  end
50
-
50
+
51
51
  true
52
52
  end
53
-
53
+
54
54
  module ClassMethods
55
55
  def schema_attributes
56
56
  @schema_attrs
57
57
  end
58
-
58
+
59
+ def schema_attr_expand(prefix)
60
+ candidates = @schema_attrs.keys.select { |a| a.to_s.start_with? prefix }
61
+ raise KeyError.new("no attribute is prefixed with #{prefix}") if candidates.size == 0
62
+ raise KeyError.new("prefix #{prefix} is not unique") if candidates.size > 1
63
+ candidates[0]
64
+ end
65
+
59
66
  def schema_attr_remove(name)
60
- raise KeyError.new("attribute #{name}: does not exist in class #{self.name}") unless @schema_attrs.include? name
67
+ raise KeyError.new("attribute #{name}: does not exist in class #{self.name}") unless @schema_attrs.include? name
61
68
  @schema_attrs.delete(name)
62
69
  self.send(:remove_method, name)
63
70
  self.send(:remove_method, "#{name}=".to_s)
64
71
  end
65
-
72
+
66
73
  def schema_attr_replace(name, opts)
67
- raise KeyError.new("attribute #{name}: does not exist in class #{self.name}") unless @schema_attrs.include? name
74
+ raise KeyError.new("attribute #{name}: does not exist in class #{self.name}") unless @schema_attrs.include? name
68
75
  schema_attr_remove(name)
69
76
  schema_attr(name, opts)
70
77
  end
71
-
78
+
72
79
  def schema_attr_update(name, opts)
73
- raise KeyError.new("attribute #{name}: does not exist in class #{self.name}") unless @schema_attrs.include? name
80
+ raise KeyError.new("attribute #{name}: does not exist in class #{self.name}") unless @schema_attrs.include? name
74
81
  raise KeyError.new("attribute #{name}: cannot update non-settable attribute") unless @schema_attrs[name][:settable]
75
82
  schema_attr_replace(name, @schema_attrs[name].merge(opts))
76
83
  end
77
-
78
- def schema_attr(name, opts)
84
+
85
+ def schema_attr(name, opts)
79
86
  @schema_attrs ||= {}
80
87
 
81
88
  raise TypeError.new("attribute #{name}: missing or invalid :class") unless opts[:class].is_a?(Class)
82
-
89
+
83
90
  if opts[:default].nil?
84
91
  opts[:default] = case opts[:class].to_s # can't case on opts[:class], because class of opts[:class] is always Class :-)
85
92
  when 'String'
@@ -92,31 +99,31 @@ module Schema
92
99
  raise ArgumentError.new("Sorry, no default default exists for #{opts[:class]}")
93
100
  end
94
101
  end
95
-
102
+
96
103
  unless opts[:default].is_a?(opts[:class]) || opts[:default].is_a?(Proc)
97
104
  raise TypeError.new("attribute #{name}: invalid :default")
98
105
  end
99
-
106
+
100
107
  if opts[:validate]
101
108
  unless opts[:validate].is_a?(Array) || opts[:validate].is_a?(Proc)
102
109
  raise ArgumentError.new("attribute #{name}: expecting Array or Proc for :validate")
103
110
  end
104
-
111
+
105
112
  if opts[:validate].is_a?(Array)
106
113
  raise TypeError.new("attribute #{name}: wrong type in :validate Array") unless opts[:validate].all? { |v| v.is_a?(opts[:class]) }
107
114
  end
108
115
  end
109
116
 
110
117
  raise ArgumentError.new("attribute #{name}: already exists") if @schema_attrs[name]
111
-
118
+
112
119
  @schema_attrs[name] = opts
113
-
120
+
114
121
  value_getter = if opts[:default].is_a?(Proc)
115
122
  %Q(
116
123
  opts = self.class.schema_attributes[:#{name}]
117
124
  value = opts[:default].call
118
- raise TypeError.new("attribute #{name}: expected type #{opts[:class]}, received \#{value.class}") unless opts[:class] == value.class
119
- )
125
+ raise TypeError.new("attribute #{name}: expected type #{opts[:class]}, received \#{value.class}") unless opts[:class] == value.class
126
+ )
120
127
  else
121
128
  %Q(value = #{opts[:default].inspect})
122
129
  end
@@ -129,7 +136,7 @@ module Schema
129
136
  @#{name}
130
137
  end
131
138
  )
132
-
139
+
133
140
  unless opts[:coerce] == false # possible values are false=no-coerce, nil=default-coerce, Proc=custom-coerce
134
141
  case opts[:class].to_s # can't case on opts[:class], because class of opts[:class] is always Class :-)
135
142
  when 'Fixnum'
@@ -155,16 +162,16 @@ module Schema
155
162
  value = Time.parse(value)
156
163
  rescue ArgumentError
157
164
  raise TypeError.new("attribute #{name}: cannot coerce from \#{value.class}")
158
- end
165
+ end
159
166
  end
160
167
  value
161
168
  end
162
- end
169
+ end
163
170
  end
164
-
171
+
165
172
  coerce = 'value = opts[:coerce].call(value)' if opts[:coerce]
166
173
  end
167
-
174
+
168
175
  unless opts[:transform] == false # possible values are false=no-transform, nil=default-transform, Proc=custom-transform
169
176
  case opts[:class].to_s # can't case on opts[:class], because class of opts[:class] is always Class :-)
170
177
  when 'String'
@@ -179,25 +186,25 @@ module Schema
179
186
  # foo.a_time = Time.new
180
187
  # Time.parse(foo.a_time.to_s) == foo.a_time # returns false most of the time!
181
188
  opts[:transform] = lambda { |t| Time.new t.year, t.mon, t.day, t.hour, t.min, t.sec, t.utc_offset }
182
- end
189
+ end
183
190
  end
184
-
191
+
185
192
  transform = 'value = opts[:transform].call(value)' if opts[:transform]
186
193
  end
187
-
194
+
188
195
  callback = %Q(
189
196
  if self.respond_to? :schema_attribute_change
190
197
  self.schema_attribute_change(:#{name}, @#{name}, value)
191
198
  end
192
199
  )
193
-
200
+
194
201
  setter_method = %Q(
195
202
  def #{name}=(value)
196
203
  opts = self.class.schema_attributes[:#{name}]
197
204
  #{coerce}
198
205
  raise TypeError.new("attribute #{name}: expected type #{opts[:class]}, received \#{value.class}") unless opts[:class] == value.class
199
206
  #{transform}
200
- #{callback}
207
+ #{callback}
201
208
  @#{name} = value
202
209
  end
203
210
  )
data/lib/taco/taco.rb CHANGED
@@ -8,33 +8,33 @@ class Taco
8
8
  HOME_DIR = '.taco'
9
9
 
10
10
  attr_accessor :home
11
-
11
+
12
12
  class NotFound < Exception; end
13
13
  class Ambiguous < Exception; end
14
-
15
- def initialize(root_path=nil)
14
+
15
+ def initialize(root_path=nil)
16
16
  @home = File.join(root_path || Dir.getwd, HOME_DIR)
17
- end
18
-
17
+ end
18
+
19
19
  def init!
20
20
  raise IOError.new("Could not create #{@home}\nDirectory already exists.") if File.exists?(@home)
21
21
 
22
22
  FileUtils.mkdir_p(@home)
23
-
23
+
24
24
  "Initialized #{@home}"
25
25
  end
26
-
26
+
27
27
  def write!(issue_or_issues)
28
28
  issues = issue_or_issues.is_a?(Array) ? issue_or_issues : [ issue_or_issues ]
29
-
29
+
30
30
  issues.each do |issue|
31
- the_json = issue.to_json # do this first so we don't bother the filesystem if the issue is invalid
31
+ the_json = issue.to_json # do this first so we don't bother the filesystem if the issue is invalid
32
32
  open(File.join(@home, issue.id), 'w') { |f| f.write(the_json) }
33
33
  end
34
-
34
+
35
35
  issue_or_issues
36
- end
37
-
36
+ end
37
+
38
38
  def read(issue_id)
39
39
  issue_path = File.join(@home, issue_id)
40
40
 
@@ -47,19 +47,19 @@ class Taco
47
47
  issue = read(File.basename(entry))
48
48
  "#{issue.id} : #{issue.summary}"
49
49
  end
50
- raise Ambiguous.new("Found several matching issues:\n%s" % issue_list.join("\n"))
50
+ raise Ambiguous.new("Found several matching issues:\n%s" % issue_list.join("\n"))
51
51
  end
52
52
 
53
53
  issue_path = entries[0]
54
54
  issue_id = File.basename entries[0]
55
55
  end
56
-
56
+
57
57
  the_json = open(issue_path) { |f| f.read }
58
58
 
59
59
  issue = Issue.from_json the_json
60
-
60
+
61
61
  raise Issue::Invalid.new("Issue ID does not match filename: #{issue.id} != #{issue_id}") unless issue.id == issue_id
62
-
62
+
63
63
  issue
64
64
  end
65
65
 
@@ -67,45 +67,46 @@ class Taco
67
67
  filter_match = if opts.fetch(:filters, []).size > 0
68
68
  conditions = opts[:filters].map do |filter|
69
69
  attr, val = filter.split(':')
70
+ attr = Issue.schema_attr_expand(attr)
70
71
  %Q|i.send("#{attr}").to_s.downcase == "#{val.downcase}"|
71
72
  end.join ' && '
72
-
73
+
73
74
  # FIXME: eval-ing user input? madness!
74
75
  eval "Proc.new { |i| #{conditions} }"
75
76
  else
76
77
  nil
77
78
  end
78
-
79
+
79
80
  ids = Dir.glob("#{@home}/*")
80
-
81
+
81
82
  ids.map do |name|
82
83
  id = File.basename name
83
84
  issue = Issue.from_json(open(name) { |f| f.read })
84
85
 
85
86
  next unless filter_match.nil? || filter_match.call(issue)
86
-
87
+
87
88
  raise Issue::Invalid.new("Issue ID does not match filename: #{issue.id} != #{id}") unless issue.id == id
88
-
89
+
89
90
  the_short_id = 8.upto(id.size).each do |n|
90
91
  the_short_id = id[0...n]
91
92
  break the_short_id unless ids.count { |i| i.include? the_short_id } > 1
92
93
  end
93
94
 
94
- # because the length of the short_id is determinable only within the context of a group of issues
95
+ # because the length of the short_id is determinable only within the context of a group of issues
95
96
  # (because it must long enough to be unique), we can only define it on Issue in the context of a group
96
97
  #
97
98
  issue.instance_eval "def short_id; #{the_short_id.inspect}; end"
98
-
99
+
99
100
  issue
100
101
  end.reject(&:nil?).sort
101
102
  end
102
103
  end
103
104
 
104
105
  class IssueEditor
105
- def initialize(taco, retry_path)
106
+ def initialize(taco, retry_path)
106
107
  @taco, @retry_path = taco, retry_path
107
108
  end
108
-
109
+
109
110
  def new_issue!(opts={})
110
111
  if opts[:from_file]
111
112
  text = open(opts[:from_file]) { |f| f.read }
@@ -116,13 +117,13 @@ class IssueEditor
116
117
 
117
118
  write_issue!(Issue.from_template(text), text) if text
118
119
  end
119
-
120
+
120
121
  def edit_issue!(issue, opts={})
121
122
  if text = invoke_editor(opts[:template] || issue.to_template)
122
123
  write_issue!(issue.update_from_template!(text), text)
123
124
  end
124
125
  end
125
-
126
+
126
127
  private
127
128
  def write_issue!(issue, text)
128
129
  begin
@@ -131,14 +132,14 @@ class IssueEditor
131
132
  open(@retry_path, 'w') { |f| f.write(text) } if text
132
133
  raise e
133
134
  end
134
-
135
+
135
136
  File.unlink @retry_path rescue nil
136
- issue
137
+ issue
137
138
  end
138
-
139
+
139
140
  def invoke_editor(template)
140
141
  text = nil
141
- file = Tempfile.new('taco')
142
+ file = Tempfile.new('taco')
142
143
 
143
144
  begin
144
145
  file.write(template)
@@ -147,7 +148,7 @@ class IssueEditor
147
148
  cmd = "$EDITOR #{file.path}"
148
149
  system(cmd)
149
150
 
150
- open(file.path) do |f|
151
+ open(file.path) do |f|
151
152
  text = f.read
152
153
  end
153
154
  ensure
@@ -4,30 +4,40 @@
4
4
 
5
5
  class TacoProfile
6
6
  attr_reader :sort_order, :filters, :columns
7
-
7
+
8
8
  def initialize(text)
9
9
  text ||= ''
10
-
11
- # FIXME: this is way too coupled to Issue
12
- #
13
- @sort_order = [ :created_at, :id ]
14
- @columns = [ :short_id, :priority, :summary ]
15
- @filters = []
16
-
10
+
11
+ @sort_order, @columns, @filters = nil, nil, nil
12
+
17
13
  text.lines.each_with_index do |line, index|
18
14
  next if line =~ /^#/ || line =~ /^\s*$/
19
-
15
+
20
16
  key, value = line.split(':', 2).map(&:strip)
21
17
  case key
22
18
  when 'sort'
19
+ raise ArgumentError.new("sort defined more than once on line #{index+1}") if @sort_order
23
20
  @sort_order = value.split(',').map(&:to_sym)
21
+ raise ArgumentError.new("Unknown Issue attribute in sort on line #{index+1}") unless @sort_order.all? { |attr| Issue.schema_attributes.include?(attr) }
24
22
  when 'filters'
23
+ raise ArgumentError.new("filters defined more than once on line #{index+1}") if @filters
25
24
  @filters = value.split(/\s/)
25
+ raise ArgumentError.new("Unknown Issue attribute in filters on line #{index+1}") unless @filters.all? { |token| attr, value = token.split(':'); Issue.schema_attributes.include?(attr.to_sym) }
26
26
  when 'columns'
27
+ raise ArgumentError.new("columns defined more than once on line #{index+1}") if @columns
27
28
  @columns = value.split(',').map(&:to_sym)
29
+ raise ArgumentError.new("Unknown Issue attribute in columns on line #{index+1}") unless @columns.all? do |attr|
30
+ # FIXME: really? hard-code :short_id? there's got to be a better way.
31
+ #
32
+ attr == :short_id || Issue.schema_attributes.include?(attr)
33
+ end
28
34
  else
29
35
  raise ArgumentError.new("Parse error on line #{index+1}")
30
36
  end
31
37
  end
38
+
39
+ @sort_order ||= [ :created_at, :id ]
40
+ @columns ||= [ :short_id, :priority, :summary ]
41
+ @filters ||= []
32
42
  end
33
43
  end
data/lib/taco/tacorc.rb CHANGED
@@ -3,23 +3,22 @@
3
3
 
4
4
  class TacoRc
5
5
  class ParseError < Exception; end
6
-
6
+
7
7
  def initialize(path)
8
8
  raise ArgumentError.new("no such file: #{path}") unless File.exists? path
9
9
  @path = path
10
10
  end
11
-
11
+
12
12
  def update_schema!(schema)
13
13
  open(@path) do |f|
14
14
  f.readlines.each_with_index do |line, index|
15
15
  next if line =~ /^#/ || line =~ /^\s*$/
16
-
17
16
  raise ParseError.new("Parse error on line #{index+1} of #{@path}: line does not begin with schema_attr_update") unless line =~ /^\s*schema_attr_update /
18
-
17
+
19
18
  begin
20
19
  eval "#{schema}.#{line.strip}"
21
20
  rescue KeyError, TypeError, NameError => e
22
- raise ParseError.new("Parse error on line #{index+1} of #{@path}: #{e.to_s}")
21
+ raise ParseError.new("Parse error on line #{index+1} of #{@path}: #{e.to_s}")
23
22
  end
24
23
  end
25
24
  end
metadata CHANGED
@@ -1,20 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: taco_it
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.2
5
- prerelease:
4
+ version: 1.5.3
6
5
  platform: ruby
7
6
  authors:
8
7
  - Mike Partelow
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-08-06 00:00:00.000000000 Z
11
+ date: 2013-10-28 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: commander
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ! '>='
20
18
  - !ruby/object:Gem::Version
@@ -22,7 +20,6 @@ dependencies:
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
24
  - - ! '>='
28
25
  - !ruby/object:Gem::Version
@@ -68,26 +65,25 @@ files:
68
65
  - bin/taco
69
66
  homepage: http://github.com/mikepartelow/taco
70
67
  licenses: []
68
+ metadata: {}
71
69
  post_install_message:
72
70
  rdoc_options: []
73
71
  require_paths:
74
72
  - lib
75
73
  required_ruby_version: !ruby/object:Gem::Requirement
76
- none: false
77
74
  requirements:
78
75
  - - ! '>='
79
76
  - !ruby/object:Gem::Version
80
77
  version: 1.9.3
81
78
  required_rubygems_version: !ruby/object:Gem::Requirement
82
- none: false
83
79
  requirements:
84
80
  - - ! '>='
85
81
  - !ruby/object:Gem::Version
86
82
  version: '0'
87
83
  requirements: []
88
84
  rubyforge_project:
89
- rubygems_version: 1.8.25
85
+ rubygems_version: 2.1.4
90
86
  signing_key:
91
- specification_version: 3
87
+ specification_version: 4
92
88
  summary: ! 'Taco Issue Tracker: A CLI Issue Tracker with JSON/filesystem backend'
93
89
  test_files: []