taco_it 1.5.2 → 1.5.3

Sign up to get free protection for your applications and to get access to all the features.
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: []