ditz-str 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +24 -0
- data/LICENSE +674 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/README.txt +143 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/bin/ditz-str +189 -0
- data/bugs/issue-02615b8c3dd0382c92f350ce2158ecfe94d11ef8.yaml +22 -0
- data/bugs/issue-06a3bbf35a60c4da2d8ea0fdc86164263126d6b2.yaml +22 -0
- data/bugs/issue-0c00c1d7fdffaad304e62d79d9b3d5e92547055b.yaml +38 -0
- data/bugs/issue-20dad4b4533d6d76d496fe5970098f1eb8efd561.yaml +26 -0
- data/bugs/issue-360ae6529dbc66358fde6b532cbea79ece37a670.yaml +22 -0
- data/bugs/issue-5177d61bf3c2783f71ef63e6e2c5e720247ef699.yaml +18 -0
- data/bugs/issue-695b564c210da1965a2bb38eef782178aead6952.yaml +26 -0
- data/bugs/issue-7d0ce6429a9fb5fa09ce3376a8921a5ecb7ecfe5.yaml +34 -0
- data/bugs/issue-a04462fa22ab6e1b02cfdd052d1f6c6f491f08f5.yaml +22 -0
- data/bugs/issue-bca54ca5107eabc3b281701041cc36ea0641cbdd.yaml +26 -0
- data/bugs/issue-d0c7d04b014d705c5fd865e4d487b5e5b6983c33.yaml +26 -0
- data/bugs/issue-f94b879842aa0274aa74fc2833252d4a06ec65cc.yaml +22 -0
- data/bugs/project.yaml +18 -0
- data/data/ditz-str/blue-check.png +0 -0
- data/data/ditz-str/close.rhtml +39 -0
- data/data/ditz-str/component.rhtml +38 -0
- data/data/ditz-str/dropdown.css +11 -0
- data/data/ditz-str/dropdown.js +58 -0
- data/data/ditz-str/edit_issue.rhtml +53 -0
- data/data/ditz-str/green-bar.png +0 -0
- data/data/ditz-str/green-check.png +0 -0
- data/data/ditz-str/header.gif +0 -0
- data/data/ditz-str/header_over.gif +0 -0
- data/data/ditz-str/index.rhtml +148 -0
- data/data/ditz-str/issue.rhtml +152 -0
- data/data/ditz-str/issue_table.rhtml +28 -0
- data/data/ditz-str/new_component.rhtml +28 -0
- data/data/ditz-str/new_issue.rhtml +57 -0
- data/data/ditz-str/new_release.rhtml +29 -0
- data/data/ditz-str/plugins/git-sync.rb +83 -0
- data/data/ditz-str/plugins/git.rb +153 -0
- data/data/ditz-str/plugins/issue-claiming.rb +174 -0
- data/data/ditz-str/red-check.png +0 -0
- data/data/ditz-str/release.rhtml +111 -0
- data/data/ditz-str/style.css +236 -0
- data/data/ditz-str/unassigned.rhtml +37 -0
- data/data/ditz-str/yellow-bar.png +0 -0
- data/ditz-str.gemspec +121 -0
- data/lib/ditzstr/brick.rb +251 -0
- data/lib/ditzstr/file-storage.rb +54 -0
- data/lib/ditzstr/hook.rb +67 -0
- data/lib/ditzstr/html.rb +104 -0
- data/lib/ditzstr/lowline.rb +201 -0
- data/lib/ditzstr/model-objects.rb +346 -0
- data/lib/ditzstr/model.rb +265 -0
- data/lib/ditzstr/operator.rb +593 -0
- data/lib/ditzstr/trollop.rb +614 -0
- data/lib/ditzstr/util.rb +61 -0
- data/lib/ditzstr/view.rb +16 -0
- data/lib/ditzstr/views.rb +157 -0
- data/lib/ditzstr.rb +69 -0
- data/man/ditz.1 +38 -0
- data/test/helper.rb +18 -0
- data/test/test_ditz-str.rb +7 -0
- metadata +219 -0
@@ -0,0 +1,265 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'sha1'
|
3
|
+
require "ditzstr/lowline"; include Lowline
|
4
|
+
require "ditzstr/util"
|
5
|
+
|
6
|
+
class Time
|
7
|
+
alias :old_to_yaml :to_yaml
|
8
|
+
def to_yaml(opts = {})
|
9
|
+
self.utc.old_to_yaml(opts)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module DitzStr
|
14
|
+
|
15
|
+
class ModelObject
|
16
|
+
class ModelError < StandardError; end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@values = {}
|
20
|
+
@serialized_values = {}
|
21
|
+
self.class.fields.map { |f, opts| @values[f] = [] if opts[:multi] }
|
22
|
+
end
|
23
|
+
|
24
|
+
## yamlability
|
25
|
+
def self.yaml_domain; "ditz.rubyforge.org,2008-03-06" end
|
26
|
+
def self.yaml_other_thing; name.split('::').last.dcfirst end
|
27
|
+
def to_yaml_type; "!#{self.class.yaml_domain}/#{self.class.yaml_other_thing}" end
|
28
|
+
def self.inherited subclass
|
29
|
+
YAML.add_domain_type(yaml_domain, subclass.yaml_other_thing) do |type, val|
|
30
|
+
o = subclass.new
|
31
|
+
val.each do |k, v|
|
32
|
+
m = "__serialized_#{k}="
|
33
|
+
if o.respond_to? m
|
34
|
+
o.send m, v
|
35
|
+
else
|
36
|
+
$stderr.puts "warning: unknown field #{k.inspect} in YAML for #{type}; ignoring"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
o.unchanged!
|
40
|
+
o
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
## override these two to model per-field transformations between disk and
|
45
|
+
## memory.
|
46
|
+
##
|
47
|
+
## convert disk form => memory form
|
48
|
+
def deserialized_form_of field, value
|
49
|
+
@serialized_values[field]
|
50
|
+
end
|
51
|
+
|
52
|
+
## convert memory form => disk form
|
53
|
+
def serialized_form_of field, value
|
54
|
+
@values[field]
|
55
|
+
end
|
56
|
+
|
57
|
+
## add a new field to a model object
|
58
|
+
def self.field name, opts={}
|
59
|
+
@fields ||= [] # can't use a hash because we need to preserve field order
|
60
|
+
raise ModelError, "field with name #{name} already defined" if @fields.any? { |k, v| k == name }
|
61
|
+
@fields << [name, opts]
|
62
|
+
|
63
|
+
if opts[:multi]
|
64
|
+
single_name = name.to_s.sub(/s$/, "") # oh yeah
|
65
|
+
define_method "add_#{single_name}" do |obj|
|
66
|
+
array = send(name)
|
67
|
+
raise ModelError, "already has a #{single_name} with name #{obj.name.inspect}" if obj.respond_to?(:name) && array.any? { |o| o.name == obj.name }
|
68
|
+
changed!
|
69
|
+
@serialized_values.delete name
|
70
|
+
array << obj
|
71
|
+
end
|
72
|
+
|
73
|
+
define_method "drop_#{single_name}" do |obj|
|
74
|
+
return unless @values[name].delete obj
|
75
|
+
@serialized_values.delete name
|
76
|
+
changed!
|
77
|
+
obj
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
define_method "#{name}=" do |o|
|
82
|
+
changed!
|
83
|
+
@serialized_values.delete name
|
84
|
+
@values[name] = o
|
85
|
+
end
|
86
|
+
|
87
|
+
define_method "__serialized_#{name}=" do |o|
|
88
|
+
changed!
|
89
|
+
@values.delete name
|
90
|
+
@serialized_values[name] = o
|
91
|
+
end
|
92
|
+
|
93
|
+
define_method "__serialized_#{name}" do
|
94
|
+
@serialized_values[name]
|
95
|
+
end
|
96
|
+
|
97
|
+
define_method name do
|
98
|
+
return @values[name] if @values.member?(name)
|
99
|
+
@values[name] = deserialized_form_of name, @serialized_values[name]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.field_names; @fields.map { |name, opts| name } end
|
104
|
+
class << self
|
105
|
+
attr_reader :fields, :values, :serialized_values
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.changes_are_logged
|
109
|
+
define_method(:changes_are_logged?) { true }
|
110
|
+
field :log_events, :multi => true, :ask => false
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.from fn
|
114
|
+
returning YAML::load_file(fn) do |o|
|
115
|
+
raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
|
116
|
+
o.pathname = fn if o.respond_to? :pathname=
|
117
|
+
|
118
|
+
o.class.fields.each do |f, opts|
|
119
|
+
m = "__serialized_#{f}"
|
120
|
+
if opts[:multi] && o.send(m).nil?
|
121
|
+
$stderr.puts "Warning: corrected nil multi-field #{f}"
|
122
|
+
o.send "#{m}=", []
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def to_s
|
129
|
+
"<#{self.class.name}: " + self.class.field_names.map { |f| "#{f}: " + (@values[f].to_s || @serialized_values[f]).inspect }.join(", ") + ">"
|
130
|
+
end
|
131
|
+
|
132
|
+
def inspect; to_s end
|
133
|
+
|
134
|
+
## depth-first search on all reachable ModelObjects. fuck yeah.
|
135
|
+
def each_modelobject
|
136
|
+
seen = {}
|
137
|
+
to_see = [self]
|
138
|
+
until to_see.empty?
|
139
|
+
cur = to_see.pop
|
140
|
+
seen[cur] = true
|
141
|
+
yield cur
|
142
|
+
cur.class.field_names.each do |f|
|
143
|
+
val = cur.send(f)
|
144
|
+
next if seen[val]
|
145
|
+
if val.is_a?(ModelObject)
|
146
|
+
to_see.push val
|
147
|
+
elsif val.is_a?(Array)
|
148
|
+
to_see += val.select { |v| v.is_a?(ModelObject) }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def save! fn
|
155
|
+
#FileUtils.mv fn, "#{fn}~", :force => true rescue nil
|
156
|
+
File.open(fn, "w") { |f| f.puts to_yaml }
|
157
|
+
self
|
158
|
+
end
|
159
|
+
|
160
|
+
def to_yaml opts={}
|
161
|
+
YAML::quick_emit(object_id, opts) do |out|
|
162
|
+
out.map(taguri, nil) do |map|
|
163
|
+
self.class.fields.each do |f, fops|
|
164
|
+
v = if @serialized_values.member?(f)
|
165
|
+
@serialized_values[f]
|
166
|
+
else
|
167
|
+
@serialized_values[f] = serialized_form_of f, @values[f]
|
168
|
+
end
|
169
|
+
|
170
|
+
map.add f.to_s, v
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def log what, who, comment
|
177
|
+
add_log_event([Time.now, who, what, comment || ""])
|
178
|
+
self
|
179
|
+
end
|
180
|
+
|
181
|
+
def changed?; @changed ||= false end
|
182
|
+
def changed!; @changed = true end
|
183
|
+
def unchanged!; @changed = false end
|
184
|
+
|
185
|
+
class << self
|
186
|
+
## creates the object, prompting the user when necessary. can take
|
187
|
+
## a :with => { hash } parameter for pre-filling model fields.
|
188
|
+
##
|
189
|
+
## can also take a :defaults_from => obj parameter for pre-filling model
|
190
|
+
## fields from another object with (some of) those fields. kinda like a
|
191
|
+
## bizarre interactive copy constructor.
|
192
|
+
def create_interactively opts={}
|
193
|
+
o = self.new
|
194
|
+
generator_args = opts[:args] || []
|
195
|
+
@fields.each do |name, field_opts|
|
196
|
+
val = if opts[:with] && opts[:with][name]
|
197
|
+
opts[:with][name]
|
198
|
+
elsif(found, x = generate_field_value(o, field_opts, generator_args)) && found
|
199
|
+
x
|
200
|
+
else
|
201
|
+
q = field_opts[:prompt] || name.to_s.capitalize
|
202
|
+
if field_opts[:multiline]
|
203
|
+
## multiline options currently aren't allowed to have a default
|
204
|
+
## value, so just ask.
|
205
|
+
ask_multiline q
|
206
|
+
else
|
207
|
+
default = if opts[:defaults_from] && opts[:defaults_from].respond_to?(name) && (x = opts[:defaults_from].send(name))
|
208
|
+
x
|
209
|
+
else
|
210
|
+
default = generate_field_default o, field_opts, generator_args
|
211
|
+
end
|
212
|
+
ask q, :default => default
|
213
|
+
end
|
214
|
+
end
|
215
|
+
o.send "#{name}=", val
|
216
|
+
end
|
217
|
+
o
|
218
|
+
end
|
219
|
+
|
220
|
+
## creates the object, filling in fields from 'vals', and throwing a
|
221
|
+
## ModelError when it can't find all the requisite fields
|
222
|
+
def create generator_args, vals={}
|
223
|
+
o = self.new
|
224
|
+
@fields.each do |name, opts|
|
225
|
+
val = if vals[name]
|
226
|
+
vals[name]
|
227
|
+
elsif(found, x = generate_field_value(o, opts, generator_args)) && found
|
228
|
+
x
|
229
|
+
else
|
230
|
+
raise ModelError, "missing required field #{name}"
|
231
|
+
end
|
232
|
+
o.send "#{name}=", val
|
233
|
+
end
|
234
|
+
o
|
235
|
+
end
|
236
|
+
|
237
|
+
private
|
238
|
+
|
239
|
+
## get the value for a field if it can be automatically determined
|
240
|
+
## returns [success, value] (because a successful value can be ni)
|
241
|
+
def generate_field_value o, opts, args
|
242
|
+
if opts[:generator].is_a? Proc
|
243
|
+
[true, opts[:generator].call(*args)]
|
244
|
+
elsif opts[:generator]
|
245
|
+
[true, o.send(opts[:generator], *args)]
|
246
|
+
elsif opts[:ask] == false # nil counts as true here
|
247
|
+
[true, opts[:default] || (opts[:multi] ? [] : nil)]
|
248
|
+
else
|
249
|
+
[false, nil]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
def generate_field_default o, opts, args
|
254
|
+
if opts[:default_generator].is_a? Proc
|
255
|
+
opts[:default_generator].call(*args)
|
256
|
+
elsif opts[:default_generator]
|
257
|
+
o.send opts[:default_generator], *args
|
258
|
+
elsif opts[:default]
|
259
|
+
opts[:default]
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|