ohac-ditz 0.5.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/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
|