ohac-ditz 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog +76 -0
- data/INSTALL +20 -0
- data/LICENSE +674 -0
- data/Manifest.txt +48 -0
- data/PLUGINS.txt +197 -0
- data/README.txt +146 -0
- data/Rakefile +66 -0
- data/ReleaseNotes +56 -0
- data/bin/ditz +230 -0
- data/contrib/completion/_ditz.zsh +29 -0
- data/contrib/completion/ditz.bash +38 -0
- data/lib/ditz/file-storage.rb +53 -0
- data/lib/ditz/hook.rb +67 -0
- data/lib/ditz/html.rb +107 -0
- data/lib/ditz/lowline.rb +244 -0
- data/lib/ditz/model-objects.rb +379 -0
- data/lib/ditz/model.rb +339 -0
- data/lib/ditz/operator.rb +655 -0
- data/lib/ditz/plugins/git-sync.rb +83 -0
- data/lib/ditz/plugins/git.rb +153 -0
- data/lib/ditz/plugins/issue-claiming.rb +193 -0
- data/lib/ditz/plugins/issue-labeling.rb +170 -0
- data/lib/ditz/util.rb +61 -0
- data/lib/ditz/view.rb +16 -0
- data/lib/ditz/views.rb +191 -0
- data/lib/ditz.rb +110 -0
- data/man/man1/ditz.1 +38 -0
- data/setup.rb +1585 -0
- data/share/ditz/blue-check.png +0 -0
- data/share/ditz/component.rhtml +24 -0
- data/share/ditz/green-bar.png +0 -0
- data/share/ditz/green-check.png +0 -0
- data/share/ditz/index.rhtml +130 -0
- data/share/ditz/issue.rhtml +119 -0
- data/share/ditz/issue_table.rhtml +28 -0
- data/share/ditz/red-check.png +0 -0
- data/share/ditz/release.rhtml +98 -0
- data/share/ditz/style.css +226 -0
- data/share/ditz/unassigned.rhtml +23 -0
- data/share/ditz/yellow-bar.png +0 -0
- metadata +116 -0
data/lib/ditz/model.rb
ADDED
@@ -0,0 +1,339 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require "yaml_waml"
|
3
|
+
if RUBY_VERSION >= '1.9.0'
|
4
|
+
require 'digest/sha1'
|
5
|
+
else
|
6
|
+
require 'sha1'
|
7
|
+
end
|
8
|
+
require "ditz/lowline"; include Lowline
|
9
|
+
require "ditz/util"
|
10
|
+
|
11
|
+
class Time
|
12
|
+
alias :old_to_yaml :to_yaml
|
13
|
+
def to_yaml(opts = {})
|
14
|
+
self.utc.old_to_yaml(opts)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Ditz
|
19
|
+
|
20
|
+
class ModelError < StandardError; end
|
21
|
+
|
22
|
+
class ModelObject
|
23
|
+
def initialize
|
24
|
+
@values = {}
|
25
|
+
@serialized_values = {}
|
26
|
+
self.class.fields.map { |f, opts| @values[f] = [] if opts[:multi] }
|
27
|
+
end
|
28
|
+
|
29
|
+
## override me and throw ModelErrors if necessary
|
30
|
+
def validate! whence, context
|
31
|
+
end
|
32
|
+
|
33
|
+
## yamlability
|
34
|
+
def self.yaml_domain; "ditz.rubyforge.org,2008-03-06" end
|
35
|
+
def self.yaml_other_thing; name.split('::').last.dcfirst end
|
36
|
+
def to_yaml_type; "!#{self.class.yaml_domain}/#{self.class.yaml_other_thing}" end
|
37
|
+
def self.inherited subclass
|
38
|
+
YAML.add_domain_type(yaml_domain, subclass.yaml_other_thing) do |type, val|
|
39
|
+
o = subclass.new
|
40
|
+
val.each do |k, v|
|
41
|
+
m = "__serialized_#{k}="
|
42
|
+
if o.respond_to? m
|
43
|
+
o.send m, v
|
44
|
+
end
|
45
|
+
end
|
46
|
+
o.class.fields.each do |f, opts|
|
47
|
+
m = "__serialized_#{f}"
|
48
|
+
if opts[:multi] && o.send(m).nil?
|
49
|
+
o.send(m + '=', [])
|
50
|
+
end
|
51
|
+
end
|
52
|
+
o.unchanged!
|
53
|
+
o
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
## override these two to model per-field transformations between disk and
|
58
|
+
## memory.
|
59
|
+
##
|
60
|
+
## convert disk form => memory form
|
61
|
+
def deserialized_form_of field, value
|
62
|
+
value
|
63
|
+
end
|
64
|
+
|
65
|
+
## convert memory form => disk form
|
66
|
+
def serialized_form_of field, value
|
67
|
+
value
|
68
|
+
end
|
69
|
+
|
70
|
+
## Add a field to a model object
|
71
|
+
##
|
72
|
+
## The options you specify here determine how the field is populated when an
|
73
|
+
## instance of this object is created. Objects can be created interactively,
|
74
|
+
## with #create_interactively, or non-interactively, with #create, and the
|
75
|
+
## creation mode, combined with these options, determine how the field is
|
76
|
+
## populated on a new model object.
|
77
|
+
##
|
78
|
+
## The default behavior is to simply prompt the user with the field name when
|
79
|
+
## in interactive mode, and to raise an exception if the value is not passed
|
80
|
+
## to #create in non-interactive mode.
|
81
|
+
##
|
82
|
+
## Options:
|
83
|
+
## :interactive_generator => a method name or Proc that will be called to
|
84
|
+
## return the value of this field, if the model object is created
|
85
|
+
## interactively.
|
86
|
+
## :generator => a method name or Proc that will be called to return the
|
87
|
+
## value of this field. If the model object is created interactively, and
|
88
|
+
## a :interactive_generator option is specified, that will be used instead.
|
89
|
+
## :multi => a boolean determining whether the field has multiple values,
|
90
|
+
## i.e., is an array. If created with :ask => false, will be initialized
|
91
|
+
## to [] instead of to nil. Additionally, the model object will have
|
92
|
+
## #add_<field> and #drop_<field> methods.
|
93
|
+
## :ask => a boolean determining whether, if the model object is created
|
94
|
+
## interactively, the user will be prompted for the value of this field.
|
95
|
+
## TRUE BY DEFAULT. If :interactive_generator or :generator are specified,
|
96
|
+
## those will be called instead.
|
97
|
+
##
|
98
|
+
## If this is true, non-interactive creation
|
99
|
+
## will raise an exception unless the field value is passed as an argument.
|
100
|
+
## If this is false, non-interactive creation will initialize this to nil
|
101
|
+
## (or [] if this field is additionally marked :multi) unless the value is
|
102
|
+
## passed as an argument.
|
103
|
+
## :prompt => a string to display to the user when prompting for the field
|
104
|
+
## value during interactive creation. Not used if :generator or
|
105
|
+
## :interactive_generator is specified.
|
106
|
+
## :multiline => a boolean determining whether to prompt the user for a
|
107
|
+
## multiline answer during interactive creation. Default false. Not used
|
108
|
+
## if :generator or :interactive_generator is specified.
|
109
|
+
## :default => a default value when prompting for the field value during
|
110
|
+
## interactive creation. Not used if :generator, :interactive_generator,
|
111
|
+
## :multiline, or :default_generator is specified.
|
112
|
+
## :default_generator => a method name or Proc which will be called to
|
113
|
+
## generate the default value when prompting for the field value during
|
114
|
+
## interactive creation. Not used if :generator, :interactive_generator,
|
115
|
+
## or :multiline is specified.
|
116
|
+
## :nil_ok => a boolean determining whether, if created in non-interactive
|
117
|
+
## mode and the value for this field is not passed in, (or is passed in
|
118
|
+
## as nil), that's ok. Default is false. This is not necessary if :ask =>
|
119
|
+
## false is specified; it's only necessary for fields that you want an
|
120
|
+
## interactive prompt for, but a nil value is fine.
|
121
|
+
def self.field name, opts={}
|
122
|
+
@fields ||= [] # can't use a hash because we need to preserve field order
|
123
|
+
raise ModelError, "field with name #{name} already defined" if @fields.any? { |k, v| k == name }
|
124
|
+
@fields << [name, opts]
|
125
|
+
|
126
|
+
if opts[:multi]
|
127
|
+
single_name = name.to_s.sub(/s$/, "") # oh yeah
|
128
|
+
define_method "add_#{single_name}" do |obj|
|
129
|
+
array = send(name)
|
130
|
+
raise ModelError, "already has a #{single_name} with name #{obj.name.inspect}" if obj.respond_to?(:name) && array.any? { |o| o.name == obj.name }
|
131
|
+
changed!
|
132
|
+
@serialized_values.delete name
|
133
|
+
array << obj
|
134
|
+
end
|
135
|
+
|
136
|
+
define_method "drop_#{single_name}" do |obj|
|
137
|
+
return unless send(name).delete obj
|
138
|
+
@serialized_values.delete name
|
139
|
+
changed!
|
140
|
+
obj
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
define_method "#{name}=" do |o|
|
145
|
+
changed!
|
146
|
+
@serialized_values.delete name
|
147
|
+
@values[name] = o
|
148
|
+
end
|
149
|
+
|
150
|
+
define_method "__serialized_#{name}=" do |o|
|
151
|
+
changed!
|
152
|
+
@values.delete name
|
153
|
+
@serialized_values[name] = o
|
154
|
+
end
|
155
|
+
|
156
|
+
define_method "__serialized_#{name}" do
|
157
|
+
@serialized_values[name]
|
158
|
+
end
|
159
|
+
|
160
|
+
define_method name do
|
161
|
+
return @values[name] if @values.member?(name)
|
162
|
+
@values[name] = deserialized_form_of name, @serialized_values[name]
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.field_names; @fields.map { |name, opts| name } end
|
167
|
+
class << self
|
168
|
+
attr_reader :fields, :values, :serialized_values
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.changes_are_logged
|
172
|
+
define_method(:changes_are_logged?) { true }
|
173
|
+
field :log_events, :multi => true, :ask => false
|
174
|
+
define_method(:log) do |what, who, comment|
|
175
|
+
add_log_event([Time.now, who, what, comment || ""])
|
176
|
+
self
|
177
|
+
end
|
178
|
+
define_method(:last_event_time) { log_events.empty? ? nil : log_events.last[0] }
|
179
|
+
end
|
180
|
+
|
181
|
+
def self.from fn
|
182
|
+
returning YAML::load_file(fn) do |o|
|
183
|
+
raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
|
184
|
+
o.pathname = fn if o.respond_to? :pathname=
|
185
|
+
o.validate! :load, []
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def to_s
|
190
|
+
"<#{self.class.name}: " + self.class.field_names.map { |f| "#{f}: " + send(f).inspect }.join(", ") + ">"
|
191
|
+
end
|
192
|
+
|
193
|
+
def inspect; to_s end
|
194
|
+
|
195
|
+
## depth-first search on all reachable ModelObjects. fuck yeah.
|
196
|
+
def each_modelobject
|
197
|
+
seen = {}
|
198
|
+
to_see = [self]
|
199
|
+
until to_see.empty?
|
200
|
+
cur = to_see.pop
|
201
|
+
seen[cur] = true
|
202
|
+
yield cur
|
203
|
+
cur.class.field_names.each do |f|
|
204
|
+
val = cur.send(f)
|
205
|
+
next if seen[val]
|
206
|
+
if val.is_a?(ModelObject)
|
207
|
+
to_see.push val
|
208
|
+
elsif val.is_a?(Array)
|
209
|
+
to_see += val.select { |v| v.is_a?(ModelObject) }
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def save! fn
|
216
|
+
#FileUtils.mv fn, "#{fn}~", :force => true rescue nil
|
217
|
+
File.open(fn, "w") { |f| f.puts to_yaml }
|
218
|
+
unchanged!
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
def to_yaml opts={}
|
223
|
+
ret = YAML::quick_emit(object_id, opts) do |out|
|
224
|
+
out.map(taguri, nil) do |map|
|
225
|
+
self.class.fields.each do |f, fops|
|
226
|
+
v = if @serialized_values.member?(f)
|
227
|
+
@serialized_values[f]
|
228
|
+
else
|
229
|
+
@serialized_values[f] = serialized_form_of f, @values[f]
|
230
|
+
end
|
231
|
+
|
232
|
+
map.add f.to_s, v
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
YamlWaml.decode(ret)
|
237
|
+
end
|
238
|
+
|
239
|
+
def changed?; @changed ||= false end
|
240
|
+
def changed!; @changed = true end
|
241
|
+
def unchanged!; @changed = false end
|
242
|
+
|
243
|
+
class << self
|
244
|
+
## creates the object, prompting the user when necessary. can take
|
245
|
+
## a :with => { hash } parameter for pre-filling model fields.
|
246
|
+
##
|
247
|
+
## can also take a :defaults_from => obj parameter for pre-filling model
|
248
|
+
## fields from another object with (some of) those fields. kinda like a
|
249
|
+
## bizarre interactive copy constructor.
|
250
|
+
def create_interactively opts={}
|
251
|
+
o = self.new
|
252
|
+
generator_args = opts[:args] || []
|
253
|
+
@fields.each do |name, field_opts|
|
254
|
+
val = if opts[:with] && opts[:with][name]
|
255
|
+
opts[:with][name]
|
256
|
+
else
|
257
|
+
found, v = generate_field_value(o, field_opts, generator_args, :interactive => true)
|
258
|
+
if found
|
259
|
+
v
|
260
|
+
else
|
261
|
+
q = field_opts[:prompt] || name.to_s.capitalize
|
262
|
+
if field_opts[:multiline]
|
263
|
+
## multiline options currently aren't allowed to have a default
|
264
|
+
## value, so just ask.
|
265
|
+
ask_multiline_or_editor q
|
266
|
+
else
|
267
|
+
default = if opts[:defaults_from] && opts[:defaults_from].respond_to?(name) && (x = opts[:defaults_from].send(name))
|
268
|
+
x
|
269
|
+
else
|
270
|
+
default = generate_field_default o, field_opts, generator_args
|
271
|
+
end
|
272
|
+
ask q, :default => default
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
o.send "#{name}=", val
|
277
|
+
end
|
278
|
+
o.validate! :create, generator_args
|
279
|
+
o
|
280
|
+
end
|
281
|
+
|
282
|
+
## creates the object, filling in fields from 'vals', and throwing a
|
283
|
+
## ModelError when it can't find all the requisite fields
|
284
|
+
def create vals={}, generator_args=[]
|
285
|
+
o = self.new
|
286
|
+
@fields.each do |fname, fopts|
|
287
|
+
x = vals[fname]
|
288
|
+
x = vals[fname.to_s] if x.nil?
|
289
|
+
val = unless x.nil?
|
290
|
+
x
|
291
|
+
else
|
292
|
+
found, x = generate_field_value(o, fopts, generator_args, :interactive => false)
|
293
|
+
if found
|
294
|
+
x
|
295
|
+
elsif !fopts[:nil_ok]
|
296
|
+
raise ModelError, "missing required field #{fname.inspect} on #{self.name} object (got #{vals.keys.inspect})"
|
297
|
+
end
|
298
|
+
end
|
299
|
+
o.send "#{fname}=", val unless val.nil?
|
300
|
+
end
|
301
|
+
o.validate! :create, generator_args
|
302
|
+
o
|
303
|
+
end
|
304
|
+
|
305
|
+
private
|
306
|
+
|
307
|
+
## get the value for a field if it can be automatically determined
|
308
|
+
## returns [success, value] (because a successful value can be nil)
|
309
|
+
def generate_field_value o, field_opts, args, opts={}
|
310
|
+
gen = if opts[:interactive]
|
311
|
+
field_opts[:interactive_generator] || field_opts[:generator]
|
312
|
+
else
|
313
|
+
field_opts[:generator]
|
314
|
+
end
|
315
|
+
|
316
|
+
if gen.is_a? Proc
|
317
|
+
[true, gen.call(*args)]
|
318
|
+
elsif gen
|
319
|
+
[true, o.send(gen, *args)]
|
320
|
+
elsif field_opts[:ask] == false # nil counts as true here
|
321
|
+
[true, field_opts[:default] || (field_opts[:multi] ? [] : nil)]
|
322
|
+
else
|
323
|
+
[false, nil]
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def generate_field_default o, field_opts, args
|
328
|
+
if field_opts[:default_generator].is_a? Proc
|
329
|
+
field_opts[:default_generator].call(*args)
|
330
|
+
elsif field_opts[:default_generator]
|
331
|
+
o.send field_opts[:default_generator], *args
|
332
|
+
elsif field_opts[:default]
|
333
|
+
field_opts[:default]
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|