ditz 0.2 → 0.3

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.
@@ -9,13 +9,9 @@
9
9
 
10
10
  <%= link_to "index", "&laquo; #{project.name} project page" %>
11
11
 
12
- <h1><%= issue.title %></h1>
12
+ <h1><%= link_issue_names project, issue.title %></h1>
13
13
 
14
- <%=
15
- project.issues.inject(p(issue.desc)) do |s, i|
16
- s.gsub(/\{issue #{i.id}\}/, link_to(i, i.title))
17
- end.gsub(/\{issue \w+\}/, "[unknown issue]")
18
- %>
14
+ <%= link_issue_names project, p(issue.desc) %>
19
15
 
20
16
  <table>
21
17
  <tr>
@@ -95,7 +91,7 @@
95
91
  <tr><td colspan="3" class="logcomment">
96
92
  <% if comment.empty? %>
97
93
  <% else %>
98
- <%=p comment %>
94
+ <%= link_issue_names project, p(comment) %>
99
95
  <% end %>
100
96
  </td></tr>
101
97
  <% end %>
@@ -3,7 +3,7 @@ require "util"
3
3
 
4
4
  class Numeric
5
5
  def to_pretty_s
6
- %w(no one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen)[self] || to_s
6
+ %w(zero one two three four five six seven eight nine ten)[self] || to_s
7
7
  end
8
8
  end
9
9
 
@@ -12,12 +12,17 @@ class NilClass
12
12
  end
13
13
 
14
14
  class String
15
- def ucfirst; self[0..0].upcase + self[1..-1] end
16
15
  def dcfirst; self[0..0].downcase + self[1..-1] end
17
16
  def blank?; self =~ /\A\s*\z/ end
18
17
  def underline; self + "\n" + ("-" * self.length) end
19
- def multiline prefix=""; blank? ? "" : "\n" + self.gsub(/^/, prefix) end
20
- def pluralize n; n.to_pretty_s + " " + (n == 1 ? self : self + "s") end # oh yeah
18
+ def multiline prefix="", cleanstart=true
19
+ return "" if blank?
20
+ (cleanstart ? "\n" : "") + gsub(/^/, prefix)
21
+ end
22
+ def pluralize n, b=true
23
+ s = (n == 1 ? self : (self == 'bugfix' ? 'bugfixes' : self + "s")) # oh yeah
24
+ b ? n.to_pretty_s + " " + s : s
25
+ end
21
26
  def multistrip; strip.gsub(/\n\n+/, "\n\n") end
22
27
  end
23
28
 
@@ -82,8 +87,13 @@ module Lowline
82
87
  end
83
88
 
84
89
  while true
85
- print [q, default_s, tail].compact.join
86
- ans = gets.strip
90
+ prompt = [q, default_s, tail].compact.join
91
+ if Ditz::has_readline?
92
+ ans = Readline::readline(prompt)
93
+ else
94
+ print prompt
95
+ ans = gets.strip
96
+ end
87
97
  if opts[:default]
88
98
  ans = opts[:default] if ans.blank?
89
99
  else
@@ -109,16 +119,28 @@ module Lowline
109
119
  puts "#{q} (ctrl-d, ., or /stop to stop, /edit to edit, /reset to reset):"
110
120
  ans = ""
111
121
  while true
112
- print "> "
113
- case(line = gets) && line.strip!
114
- when /^\.$/, nil, "/stop"
115
- break
116
- when "/reset"
117
- return ask_multiline(q)
118
- when "/edit"
119
- return ask_via_editor(q, ans)
122
+ if Ditz::has_readline?
123
+ line = Readline::readline('> ')
124
+ else
125
+ (line = gets) && line.strip!
126
+ end
127
+ if line
128
+ if Ditz::has_readline?
129
+ Readline::HISTORY.push(line)
130
+ end
131
+ case line
132
+ when /^\.$/, "/stop"
133
+ break
134
+ when "/reset"
135
+ return ask_multiline(q)
136
+ when "/edit"
137
+ return ask_via_editor(q, ans)
138
+ else
139
+ ans << line + "\n"
140
+ end
120
141
  else
121
- ans << line + "\n"
142
+ puts
143
+ break
122
144
  end
123
145
  end
124
146
  ans.multistrip
@@ -148,10 +170,10 @@ module Lowline
148
170
  ans = ask "(A)dd #{name}, (r)emove #{name}, or (d)one"
149
171
  case ans
150
172
  when "a", "A"
151
- ans = ask "#{name.ucfirst} name", ""
173
+ ans = ask "#{name.capitalize} name", ""
152
174
  stuff << ans unless ans =~ /^\s*$/
153
175
  when "r", "R"
154
- ans = ask "Remove which component? (1--#{stuff.size})"
176
+ ans = ask "Remove which #{name}? (1--#{stuff.size})"
155
177
  stuff.delete_at(ans.to_i - 1) if ans
156
178
  when "d", "D"
157
179
  break
@@ -177,7 +199,7 @@ module Lowline
177
199
  end
178
200
 
179
201
  j = while true
180
- i = ask "#{name.ucfirst} (1--#{stuff.size})"
202
+ i = ask "#{name.capitalize} (1--#{stuff.size})"
181
203
  break i.to_i if i && (1 .. stuff.size).member?(i.to_i)
182
204
  end
183
205
 
@@ -4,7 +4,7 @@ module Ditz
4
4
 
5
5
  class Component < ModelObject
6
6
  field :name
7
- def name_prefix; @name.gsub(/\s+/, "-").downcase end
7
+ def name_prefix; name.gsub(/\s+/, "-").downcase end
8
8
  end
9
9
 
10
10
  class Release < ModelObject
@@ -36,6 +36,8 @@ end
36
36
  class Project < ModelObject
37
37
  class Error < StandardError; end
38
38
 
39
+ attr_accessor :pathname
40
+
39
41
  field :name, :default_generator => lambda { File.basename(Dir.pwd) }
40
42
  field :version, :default => Ditz::VERSION, :ask => false
41
43
  field :components, :multi => true, :generator => :get_components
@@ -83,6 +85,10 @@ EOS
83
85
  issues.select { |i| i.release.nil? }
84
86
  end
85
87
 
88
+ def group_issues these_issues=issues
89
+ these_issues.group_by { |i| i.type }.sort_by { |(t,g)| Issue::TYPE_ORDER[t] }
90
+ end
91
+
86
92
  def assign_issue_names!
87
93
  prefixes = components.map { |c| [c.name, c.name.gsub(/^\s+/, "-").downcase] }.to_h
88
94
  ids = components.map { |c| [c.name, 0] }.to_h
@@ -93,7 +99,7 @@ EOS
93
99
 
94
100
  def validate!
95
101
  if(dup = components.map { |c| c.name }.first_duplicate)
96
- raise Error, "more than one component named #{dup.inspect}"
102
+ raise Error, "more than one component named #{dup.inspect}: #{components.inspect}"
97
103
  elsif(dup = releases.map { |r| r.name }.first_duplicate)
98
104
  raise Error, "more than one release named #{dup.inspect}"
99
105
  end
@@ -116,27 +122,54 @@ class Issue < ModelObject
116
122
  field :id, :ask => false, :generator => :make_id
117
123
  changes_are_logged
118
124
 
119
- attr_accessor :name
125
+ attr_accessor :name, :pathname, :project
126
+
127
+ ## these are the fields we interpolate issue names on
128
+ INTERPOLATED_FIELDS = [:title, :desc, :log_events]
120
129
 
121
130
  STATUS_SORT_ORDER = { :unstarted => 2, :paused => 1, :in_progress => 0, :closed => 3 }
122
131
  STATUS_WIDGET = { :unstarted => "_", :in_progress => ">", :paused => "=", :closed => "x" }
123
132
  DISPOSITIONS = [ :fixed, :wontfix, :reorg ]
124
- TYPES = [ :bugfix, :feature ]
133
+ TYPES = [ :bugfix, :feature, :task ]
134
+ TYPE_ORDER = { :bugfix => 0, :feature => 1, :task => 2 }
135
+ TYPE_LETTER = { 'b' => :bugfix, 'f' => :feature, 't' => :task }
125
136
  STATUSES = STATUS_WIDGET.keys
126
137
 
127
138
  STATUS_STRINGS = { :in_progress => "in progress", :wontfix => "won't fix" }
128
139
  DISPOSITION_STRINGS = { :wontfix => "won't fix", :reorg => "reorganized" }
129
140
 
130
- def before_serialize project
131
- self.desc = project.issues.inject(desc) do |s, i|
132
- s.gsub(/\b#{i.name}\b/, "{issue #{i.id}}")
141
+ def serialized_form_of field, value
142
+ return super unless INTERPOLATED_FIELDS.member? field
143
+
144
+ if field == :log_events
145
+ value.map do |time, who, what, comment|
146
+ comment = @project.issues.inject(comment) do |s, i|
147
+ s.gsub(/\b#{i.name}\b/, "{issue #{i.id}}")
148
+ end
149
+ [time, who, what, comment]
150
+ end
151
+ else
152
+ @project.issues.inject(value) do |s, i|
153
+ s.gsub(/\b#{i.name}\b/, "{issue #{i.id}}")
154
+ end
133
155
  end
134
156
  end
135
157
 
136
- def interpolated_desc issues
137
- issues.inject(desc) do |s, i|
138
- s.gsub(/\{issue #{i.id}\}/, block_given? ? yield(i) : i.name)
139
- end.gsub(/\{issue \w+\}/, "[unknown issue]")
158
+ def deserialized_form_of field, value
159
+ return super unless INTERPOLATED_FIELDS.member? field
160
+
161
+ if field == :log_events
162
+ value.map do |time, who, what, comment|
163
+ comment = @project.issues.inject(comment) do |s, i|
164
+ s.gsub(/\{issue #{i.id}\}/, i.name)
165
+ end.gsub(/\{issue \w+\}/, "[unknown issue]")
166
+ [time, who, what, comment]
167
+ end
168
+ else
169
+ @project.issues.inject(value) do |s, i|
170
+ s.gsub(/\{issue #{i.id}\}/, i.name)
171
+ end.gsub(/\{issue \w+\}/, "[unknown issue]")
172
+ end
140
173
  end
141
174
 
142
175
  ## make a unique id
@@ -144,8 +177,8 @@ class Issue < ModelObject
144
177
  SHA1.hexdigest [Time.now, rand, creation_time, reporter, title, desc].join("\n")
145
178
  end
146
179
 
147
- def sort_order; [STATUS_SORT_ORDER[@status], creation_time] end
148
- def status_widget; STATUS_WIDGET[@status] end
180
+ def sort_order; [STATUS_SORT_ORDER[status], creation_time] end
181
+ def status_widget; STATUS_WIDGET[status] end
149
182
 
150
183
  def status_string; STATUS_STRINGS[status] || status.to_s end
151
184
  def disposition_string; DISPOSITION_STRINGS[disposition] || disposition.to_s end
@@ -172,7 +205,7 @@ class Issue < ModelObject
172
205
  def change_status to, who, comment
173
206
  raise Error, "unknown status #{to}" unless STATUSES.member? to
174
207
  raise Error, "already marked as #{to}" if status == to
175
- log "changed status from #{@status} to #{to}", who, comment
208
+ log "changed status from #{status} to #{to}", who, comment
176
209
  self.status = to
177
210
  end
178
211
  private :change_status
@@ -205,6 +238,11 @@ class Issue < ModelObject
205
238
  self.release = release.name
206
239
  end
207
240
 
241
+ def assign_to_component component, who, comment
242
+ log "assigned to component #{component.name} from #{self.component}", who, comment
243
+ self.component = component.name
244
+ end
245
+
208
246
  def unassign who, comment
209
247
  raise Error, "not assigned to a release" unless release
210
248
  log "unassigned from release #{release}", who, comment
@@ -212,8 +250,8 @@ class Issue < ModelObject
212
250
  end
213
251
 
214
252
  def get_type config, project
215
- type = ask "Is this a (b)ugfix or a (f)eature?", :restrict => /^[bf]$/
216
- type == "b" ? :bugfix : :feature
253
+ type = ask "Is this a (b)ugfix, a (f)eature, or a (t)ask?", :restrict => /^[bft]$/
254
+ TYPE_LETTER[type]
217
255
  end
218
256
 
219
257
  def get_component config, project
@@ -245,18 +283,20 @@ end
245
283
  class Config < ModelObject
246
284
  field :name, :prompt => "Your name", :default_generator => :get_default_name
247
285
  field :email, :prompt => "Your email address", :default_generator => :get_default_email
286
+ field :issue_dir, :ask => false, :default => "bugs"
248
287
 
249
288
  def user; "#{name} <#{email}>" end
250
289
 
251
290
  def get_default_name
252
291
  require 'etc'
253
292
 
254
- name = Etc.getpwnam(ENV["USER"]).gecos.split(/,/).first
293
+ name = Etc.getpwnam(ENV["USER"])
294
+ name = name ? name.gecos.split(/,/).first : ""
255
295
  end
256
296
 
257
297
  def get_default_email
258
298
  require 'socket'
259
- email = ENV["USER"] + "@" +
299
+ email = (ENV["USER"] || "") + "@" +
260
300
  begin
261
301
  Socket.gethostbyname(Socket.gethostname).first
262
302
  rescue SocketError
@@ -1,7 +1,7 @@
1
1
  require 'yaml'
2
+ require 'sha1'
2
3
  require "lowline"; include Lowline
3
4
  require "util"
4
- require 'sha1'
5
5
 
6
6
  class Time
7
7
  alias :old_to_yaml :to_yaml
@@ -15,47 +15,83 @@ module Ditz
15
15
  class ModelObject
16
16
  class ModelError < StandardError; end
17
17
 
18
+ def initialize
19
+ @values = {}
20
+ @serialized_values = {}
21
+ end
22
+
18
23
  ## yamlability
19
24
  def self.yaml_domain; "ditz.rubyforge.org,2008-03-06" end
20
25
  def self.yaml_other_thing; name.split('::').last.dcfirst end
21
26
  def to_yaml_type; "!#{self.class.yaml_domain}/#{self.class.yaml_other_thing}" end
22
- def to_yaml_properties; self.class.fields.map { |f| "@#{f.to_s}" } end
23
27
  def self.inherited subclass
24
28
  YAML.add_domain_type(yaml_domain, subclass.yaml_other_thing) do |type, val|
25
- YAML.object_maker(subclass, val)
29
+ o = subclass.new
30
+ val.each { |k, v| o.send "__serialized_#{k}=", v }
31
+ o.unchanged!
32
+ o
26
33
  end
27
34
  end
28
- def before_serialize(*a); end
29
- def after_deserialize(*a); end
30
35
 
36
+ ## override these two to model per-field transformations between disk and
37
+ ## memory.
38
+ ##
39
+ ## convert disk form => memory form
40
+ def deserialized_form_of field, value
41
+ @serialized_values[field]
42
+ end
43
+
44
+ ## convert memory form => disk form
45
+ def serialized_form_of field, value
46
+ @values[field]
47
+ end
48
+
49
+ ## add a new field to a model object
31
50
  def self.field name, opts={}
32
- @fields ||= [] # can't use a hash because want to preserve field order when serialized
51
+ @fields ||= [] # can't use a hash because we need to preserve field order
33
52
  raise ModelError, "field with name #{name} already defined" if @fields.any? { |k, v| k == name }
34
53
  @fields << [name, opts]
35
54
 
36
- attr_reader name
37
55
  if opts[:multi]
38
56
  single_name = name.to_s.sub(/s$/, "") # oh yeah
39
57
  define_method "add_#{single_name}" do |obj|
40
- array = self.instance_variable_get("@#{name}")
58
+ array = send(name)
41
59
  raise ModelError, "already has a #{single_name} with name #{obj.name.inspect}" if obj.respond_to?(:name) && array.any? { |o| o.name == obj.name }
42
60
  changed!
61
+ @serialized_values.delete name
43
62
  array << obj
44
63
  end
45
64
 
46
65
  define_method "drop_#{single_name}" do |obj|
47
- return unless self.instance_variable_get("@#{name}").delete obj
66
+ return unless @values[name].delete obj
67
+ @serialized_values.delete name
48
68
  changed!
49
69
  obj
50
70
  end
51
71
  end
72
+
52
73
  define_method "#{name}=" do |o|
53
74
  changed!
54
- instance_variable_set "@#{name}", o
75
+ @serialized_values.delete name
76
+ @values[name] = o
77
+ end
78
+
79
+ define_method "__serialized_#{name}=" do |o|
80
+ changed!
81
+ @values.delete name
82
+ @serialized_values[name] = o
83
+ end
84
+
85
+ define_method name do
86
+ return @values[name] if @values.member?(name)
87
+ @values[name] = deserialized_form_of name, @serialized_values[name]
55
88
  end
56
89
  end
57
90
 
58
- def self.fields; @fields.map { |name, opts| name } end
91
+ def self.field_names; @fields.map { |name, opts| name } end
92
+ class << self
93
+ attr_reader :fields, :values, :serialized_values
94
+ end
59
95
 
60
96
  def self.changes_are_logged
61
97
  define_method(:changes_are_logged?) { true }
@@ -65,9 +101,16 @@ class ModelObject
65
101
  def self.from fn
66
102
  returning YAML::load_file(fn) do |o|
67
103
  raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
104
+ o.pathname = fn if o.respond_to? :pathname=
68
105
  end
69
106
  end
70
107
 
108
+ def to_s
109
+ "<#{self.class.name}: " + self.class.field_names.map { |f| "#{f}: " + (@values[f].to_s || @serialized_values[f]).inspect }.join(", ") + ">"
110
+ end
111
+
112
+ def inspect; to_s end
113
+
71
114
  ## depth-first search on all reachable ModelObjects. fuck yeah.
72
115
  def each_modelobject
73
116
  seen = {}
@@ -76,7 +119,7 @@ class ModelObject
76
119
  cur = to_see.pop
77
120
  seen[cur] = true
78
121
  yield cur
79
- cur.class.fields.each do |f|
122
+ cur.class.field_names.each do |f|
80
123
  val = cur.send(f)
81
124
  next if seen[val]
82
125
  if val.is_a?(ModelObject)
@@ -91,15 +134,33 @@ class ModelObject
91
134
  def save! fn
92
135
  #FileUtils.mv fn, "#{fn}~", :force => true rescue nil
93
136
  File.open(fn, "w") { |f| f.puts to_yaml }
137
+ self
94
138
  end
95
139
 
140
+ def to_yaml opts={}
141
+ YAML::quick_emit(object_id, opts) do |out|
142
+ out.map(taguri, nil) do |map|
143
+ self.class.fields.each do |f, fops|
144
+ v = if @serialized_values.member?(f)
145
+ @serialized_values[f]
146
+ else
147
+ @serialized_values[f] = serialized_form_of f, @values[f]
148
+ end
149
+
150
+ map.add f.to_s, v
151
+ end
152
+ end
153
+ end
154
+ end
155
+
96
156
  def log what, who, comment
97
- add_log_event([Time.now, who, what, comment])
157
+ add_log_event([Time.now, who, what, comment || ""])
98
158
  self
99
159
  end
100
160
 
101
161
  def changed?; @changed ||= false end
102
162
  def changed!; @changed = true end
163
+ def unchanged!; @changed = false end
103
164
 
104
165
  def self.create_interactively opts={}
105
166
  o = self.new
@@ -114,7 +175,7 @@ class ModelObject
114
175
  elsif field_opts[:ask] == false # nil counts as true here
115
176
  field_opts[:default] || (field_opts[:multi] ? [] : nil)
116
177
  else
117
- q = field_opts[:prompt] || name.to_s.ucfirst
178
+ q = field_opts[:prompt] || name.to_s.capitalize
118
179
  if field_opts[:multiline]
119
180
  ask_multiline q
120
181
  else