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 +15 -0
- data/bin/taco +2 -2
- data/lib/taco/change.rb +18 -18
- data/lib/taco/cli.rb +56 -35
- data/lib/taco/issue.rb +46 -46
- data/lib/taco/schema.rb +42 -35
- data/lib/taco/taco.rb +33 -32
- data/lib/taco/taco_profile.rb +19 -9
- data/lib/taco/tacorc.rb +4 -5
- metadata +5 -9
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.
|
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(&:
|
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
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
data/lib/taco/taco_profile.rb
CHANGED
@@ -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
|
-
|
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.
|
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-
|
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.
|
85
|
+
rubygems_version: 2.1.4
|
90
86
|
signing_key:
|
91
|
-
specification_version:
|
87
|
+
specification_version: 4
|
92
88
|
summary: ! 'Taco Issue Tracker: A CLI Issue Tracker with JSON/filesystem backend'
|
93
89
|
test_files: []
|