ursm-ditz 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/Changelog +15 -0
  2. data/INSTALL +20 -0
  3. data/LICENSE +674 -0
  4. data/Manifest.txt +40 -0
  5. data/PLUGINS.txt +140 -0
  6. data/README.txt +43 -27
  7. data/Rakefile +36 -3
  8. data/ReleaseNotes +6 -0
  9. data/bin/ditz +49 -63
  10. data/contrib/completion/ditz.bash +25 -9
  11. data/lib/ditz.rb +52 -7
  12. data/lib/ditz/file-storage.rb +54 -0
  13. data/lib/{hook.rb → ditz/hook.rb} +1 -1
  14. data/lib/{html.rb → ditz/html.rb} +42 -4
  15. data/lib/{lowline.rb → ditz/lowline.rb} +31 -11
  16. data/lib/{model-objects.rb → ditz/model-objects.rb} +53 -21
  17. data/lib/ditz/model.rb +321 -0
  18. data/lib/{operator.rb → ditz/operator.rb} +122 -67
  19. data/lib/ditz/plugins/git-sync.rb +83 -0
  20. data/lib/{plugins → ditz/plugins}/git.rb +57 -18
  21. data/lib/ditz/plugins/issue-claiming.rb +174 -0
  22. data/lib/ditz/plugins/issue-labeling.rb +161 -0
  23. data/lib/{util.rb → ditz/util.rb} +4 -0
  24. data/lib/{view.rb → ditz/view.rb} +0 -0
  25. data/lib/{views.rb → ditz/views.rb} +7 -4
  26. data/man/{ditz.1 → man1/ditz.1} +1 -1
  27. data/setup.rb +1585 -0
  28. data/share/ditz/blue-check.png +0 -0
  29. data/{lib → share/ditz}/component.rhtml +7 -5
  30. data/share/ditz/green-bar.png +0 -0
  31. data/share/ditz/green-check.png +0 -0
  32. data/share/ditz/index.rhtml +130 -0
  33. data/share/ditz/issue.rhtml +119 -0
  34. data/share/ditz/issue_table.rhtml +28 -0
  35. data/share/ditz/red-check.png +0 -0
  36. data/share/ditz/release.rhtml +98 -0
  37. data/share/ditz/style.css +226 -0
  38. data/share/ditz/unassigned.rhtml +23 -0
  39. data/share/ditz/yellow-bar.png +0 -0
  40. metadata +50 -28
  41. data/lib/index.rhtml +0 -113
  42. data/lib/issue.rhtml +0 -111
  43. data/lib/issue_table.rhtml +0 -33
  44. data/lib/model.rb +0 -208
  45. data/lib/plugins/issue-claiming.rb +0 -92
  46. data/lib/release.rhtml +0 -69
  47. data/lib/style.css +0 -127
  48. data/lib/trollop.rb +0 -518
  49. data/lib/unassigned.rhtml +0 -31
  50. data/lib/vendor/yaml_waml.rb +0 -28
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
3
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4
+
5
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
6
+ <head>
7
+ <title>Issues not assigned to any release</title>
8
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
9
+ <link rel="stylesheet" href="style.css" type="text/css" />
10
+ </head>
11
+ <body>
12
+
13
+ <div class="main">
14
+ <h1>Issues not assigned to any release</h1>
15
+ <div class="backptr"><%= link_to "index", "&laquo; #{project.name} project page" %></div>
16
+
17
+ <h2>All issues</h2>
18
+ <%= render "issue_table", :show_component => false, :show_release => true %>
19
+ </div>
20
+ <div class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.</div>
21
+
22
+ </body>
23
+ </html>
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ursm-ditz
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.4"
4
+ version: "0.5"
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Morgan
@@ -9,56 +9,78 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2008-08-21 00:00:00 -07:00
12
+ date: 2008-10-01 00:00:00 -07:00
13
13
  default_executable: ditz
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
- name: hoe
16
+ name: trollop
17
17
  version_requirement:
18
18
  version_requirements: !ruby/object:Gem::Requirement
19
19
  requirements:
20
20
  - - ">="
21
21
  - !ruby/object:Gem::Version
22
- version: 1.7.0
22
+ version: "1.9"
23
23
  version:
24
- description: "Ditz is a simple, light-weight distributed issue tracker designed to work with distributed version control systems like git, darcs, Mercurial, and Bazaar. It can also be used with centralized systems like SVN. Ditz maintains an issue database directory on disk, with files written in a line-based and human-editable format. This directory can be kept under version control, alongside project code. There are several different ways to use ditz: 1. Treat issue change the same as code change: include it as part of commits, and merge it with changes from other developers, resolving conflicts in the usual manner. 2. Keep the issue database in the repository but in a separate branch. Issue changes can be managed by your VCS, but is not tied directly to code commits. 3. Keep the issue database separate and not under VCS at all. Ditz provides a simple, console-based interface for creating and updating the issue database file, and some rudimentary static HTML generation capabilities for producing world-readable status pages (for a demo, see the ditz ditz page). It currently offers no central public method of bug submission. Synopsis: # set up project. creates the bugs.yaml file. 1. ditz init 2. ditz add-release # add an issue 3. ditz add"
24
+ - !ruby/object:Gem::Dependency
25
+ name: kakutani-yaml_waml
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: "0.3"
32
+ version:
33
+ description: "Ditz is a simple, light-weight distributed issue tracker designed to work with distributed version control systems like git, darcs, Mercurial, and Bazaar. It can also be used with centralized systems like SVN. Ditz maintains an issue database directory on disk, with files written in a line-based and human-editable format. This directory can be kept under version control, alongside project code. Ditz provides a simple, console-based interface for creating and updating the issue database files, and some basic static HTML generation capabilities for producing world-readable status pages (for a demo, see the ditz ditz page). Ditz includes a robust plugin system for adding commands, model fields, and modifying output. See PLUGINS.txt for documentation on the pre-shipped plugins. Ditz currently offers no central public method of bug submission. == USING DITZ There are several different ways to use Ditz: 1. Treat issue change the same as code change: include it as part of commits, and merge it with changes from other developers, resolving conflicts in the usual manner. 2. Keep the issue database in the repository but in a separate branch. Issue changes can be managed by your VCS, but is not tied directly to code commits. 3. Keep the issue database separate and not under VCS at all."
25
34
  email: wmorgan-ditz@masanjin.net
26
35
  executables:
27
36
  - ditz
28
37
  extensions: []
29
38
 
30
39
  extra_rdoc_files:
40
+ - Manifest.txt
41
+ - PLUGINS.txt
31
42
  - README.txt
32
43
  files:
33
44
  - Changelog
45
+ - INSTALL
46
+ - LICENSE
47
+ - Manifest.txt
48
+ - PLUGINS.txt
34
49
  - README.txt
35
50
  - Rakefile
36
51
  - ReleaseNotes
37
52
  - bin/ditz
38
- - lib/component.rhtml
39
- - lib/ditz.rb
40
- - lib/hook.rb
41
- - lib/html.rb
42
- - lib/index.rhtml
43
- - lib/issue.rhtml
44
- - lib/issue_table.rhtml
45
- - lib/lowline.rb
46
- - lib/model-objects.rb
47
- - lib/model.rb
48
- - lib/operator.rb
49
- - lib/release.rhtml
50
- - lib/trollop.rb
51
- - lib/style.css
52
- - lib/unassigned.rhtml
53
- - lib/util.rb
54
- - lib/view.rb
55
- - lib/views.rb
56
- - lib/plugins/git.rb
57
- - lib/plugins/issue-claiming.rb
58
- - lib/vendor/yaml_waml.rb
59
- - contrib/completion/ditz.bash
60
53
  - contrib/completion/_ditz.zsh
61
- - man/ditz.1
54
+ - contrib/completion/ditz.bash
55
+ - lib/ditz.rb
56
+ - lib/ditz/file-storage.rb
57
+ - lib/ditz/hook.rb
58
+ - lib/ditz/html.rb
59
+ - lib/ditz/lowline.rb
60
+ - lib/ditz/model-objects.rb
61
+ - lib/ditz/model.rb
62
+ - lib/ditz/operator.rb
63
+ - lib/ditz/plugins/git-sync.rb
64
+ - lib/ditz/plugins/git.rb
65
+ - lib/ditz/plugins/issue-claiming.rb
66
+ - lib/ditz/plugins/issue-labeling.rb
67
+ - lib/ditz/util.rb
68
+ - lib/ditz/view.rb
69
+ - lib/ditz/views.rb
70
+ - share/ditz/index.rhtml
71
+ - share/ditz/issue.rhtml
72
+ - share/ditz/issue_table.rhtml
73
+ - share/ditz/release.rhtml
74
+ - share/ditz/unassigned.rhtml
75
+ - share/ditz/component.rhtml
76
+ - share/ditz/style.css
77
+ - share/ditz/blue-check.png
78
+ - share/ditz/green-bar.png
79
+ - share/ditz/green-check.png
80
+ - share/ditz/red-check.png
81
+ - share/ditz/yellow-bar.png
82
+ - man/man1/ditz.1
83
+ - setup.rb
62
84
  has_rdoc: true
63
85
  homepage: http://ditz.rubyforge.org
64
86
  post_install_message:
@@ -1,113 +0,0 @@
1
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
-
4
- <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
5
- <head>
6
- <title><%= project.name %> Issue Tracker</title>
7
- <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
8
- <link rel="stylesheet" href="style.css" type="text/css" />
9
- </head>
10
- <body>
11
-
12
- <h1><%= project.name %> Issue Tracker</h1>
13
-
14
- <h2>Upcoming Releases</h2>
15
- <% if upcoming_releases.empty? %>
16
- <p>No upcoming releases.</p>
17
- <% else %>
18
- <ul>
19
- <% upcoming_releases.each do |r| %>
20
- <%
21
- issues = project.issues_for_release r
22
- num_done = issues.count_of { |i| i.closed? }
23
- pct_done = issues.size == 0 ? 100 : (100.0 * num_done / issues.size)
24
- open_issues = project.group_issues(issues.select { |i| i.open? })
25
- %>
26
- <li>
27
- <%= link_to r, "Release #{r.name}" %>:
28
- <% if issues.empty? %>
29
- no issues
30
- <% else %>
31
- <%= sprintf "%.0f%%", pct_done %> complete;
32
- <% if open_issues.empty? %>
33
- ready for release!
34
- <% else %>
35
- <%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
36
- <% end %>
37
- <% end %>
38
- </li>
39
- <% end %>
40
- </ul>
41
- <% end %>
42
-
43
- <h2>Past Releases</h2>
44
- <% if past_releases.empty? %>
45
- <p>No past releases.</p>
46
- <% else %>
47
- <ul>
48
- <% past_releases.sort_by { |r| r.release_time }.reverse.each do |r| %>
49
- <li><%= link_to r, "Release #{r.name}" %>, released <%= r.release_time.pretty_date %>. </li>
50
- <% end %>
51
- </ul>
52
- <% end %>
53
-
54
- <h2>Unassigned issues</h2>
55
- <%
56
- issues = project.unassigned_issues
57
- open_issues = issues.select { |i| i.open? }
58
- %>
59
- <p>
60
- <% if issues.empty? %>
61
- No unassigned issues.
62
- <% else %>
63
- <%= link_to "unassigned", "unassigned issue".pluralize(issues.size).capitalize %>; <%= open_issues.size.to_pretty_s %> of them open.
64
- <% end %>
65
- </p>
66
-
67
- <% if components.size > 1 %>
68
- <h2>Open Issues by component</h2>
69
- <ul>
70
- <% components.each do |c| %>
71
- <%
72
- open_issues = project.group_issues(project.issues_for_component(c).select { |i| i.open? })
73
- %>
74
- <li>
75
- <% if open_issues.empty? %>
76
- <span class="dimmed">
77
- <%= link_to c, c.name %>:
78
- no open issues.
79
- </span>
80
- <% else %>
81
- <%= link_to c, c.name %>:
82
- <%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
83
- <% end %>
84
- </li>
85
- <% end %>
86
- </ul>
87
- <% end %>
88
-
89
- <h2>Recent activity</h2>
90
-
91
- <table>
92
- <% project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
93
- flatten_one_level.
94
- sort_by { |e| e.first.first }.
95
- reverse[0 ... 10].
96
- each do |(date, who, what, comment), i| %>
97
- <tr>
98
- <td><%= date.pretty_date %></td>
99
- <td class="issuename">
100
- <%= link_issue_names project, h(i.name) %>
101
- <%= what %> by <%= who.shortened_email %></td>
102
- </tr>
103
- <% if comment && comment =~ /\S/ %>
104
- <tr><td></td><td><i><%= link_issue_names project, h(comment) %></i></td></tr>
105
- <% end %>
106
- <% end %>
107
- </table>
108
-
109
- <p class="footer">Generated by <a
110
- href="http://ditz.rubyforge.org/">ditz</a>.</p>
111
-
112
- </body>
113
- </html>
@@ -1,111 +0,0 @@
1
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
2
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3
-
4
- <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
5
- <head>
6
- <title><%= issue.title %></title>
7
- <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
8
- <link rel="stylesheet" href="style.css" type="text/css" />
9
- </head>
10
-
11
- <body>
12
-
13
- <div><%= link_to "index", "&laquo; #{project.name} project page" %></div>
14
-
15
- <h1><%= link_issue_names project, issue.title %></h1>
16
-
17
- <%= link_issue_names project, p(issue.desc) %>
18
-
19
- <table>
20
- <tr>
21
- <td class="attrname">Id:</td>
22
- <td class="attrval"><%= issue.id %></td>
23
- </tr>
24
-
25
- <tr>
26
- <td class="attrname">Type:</td>
27
- <td class="attrval"><%= issue.type %></td>
28
- </tr>
29
-
30
- <tr>
31
- <td class="attrname">Creation time:</td>
32
- <td class="attrval"><%= issue.creation_time %></td>
33
- </tr>
34
-
35
- <tr>
36
- <td class="attrname">Creator:</td>
37
- <td class="attrval"><%=obscured_email issue.reporter %></td>
38
- </tr>
39
-
40
- <% unless issue.references.empty? %>
41
- <tr>
42
- <td class="attrname">References:</td>
43
- <td class="attrval">
44
- <% issue.references.each_with_index do |r, i| %>
45
- [<%= i + 1 %>] <%= link_to r, r %><br/>
46
- <% end %>
47
- </td>
48
- </tr>
49
-
50
- <% end %>
51
-
52
- <tr>
53
- <td class="attrname">Release:</td>
54
- <td class="attrval">
55
- <% if release %>
56
- <%= link_to release, release.name %>
57
- <% if release.released? %>
58
- (released <%= release.release_time.pretty_date %>)
59
- <% else %>
60
- (unreleased)
61
- <% end %>
62
- <% else %>
63
- <%= link_to "unassigned", "unassigned" %>
64
- <% end %>
65
- </td>
66
- </tr>
67
-
68
- <tr>
69
- <td class="attrname">Component:</td>
70
- <td class="attrval"><%= link_to component, component.name %></td>
71
- </tr>
72
-
73
- <tr>
74
- <td class="attrname">Status:</td>
75
- <td class="attrval">
76
- <%= issue.status_string %><% if issue.closed? %>: <%= issue.disposition_string %><% end %>
77
- </td>
78
- </tr>
79
-
80
- <%= extra_summary_html %>
81
-
82
- </table>
83
-
84
- <%= extra_details_html %>
85
-
86
- <h2>Issue log</h2>
87
-
88
- <table>
89
- <% issue.log_events.each_with_index do |(time, who, what, comment), i| %>
90
- <% if i % 2 == 0 %>
91
- <tr class="logentryeven">
92
- <% else %>
93
- <tr class="logentryodd">
94
- <% end %>
95
- <td class="logtime"><%=t time %></td>
96
- <td class="logwho"><%=obscured_email who %></td>
97
- <td class="logwhat"><%=h what %></td>
98
- </tr>
99
- <tr><td colspan="3" class="logcomment">
100
- <% if comment.empty? %>
101
- <% else %>
102
- <%= link_issue_names project, p(comment) %>
103
- <% end %>
104
- </td></tr>
105
- <% end %>
106
- </table>
107
-
108
- <p class="footer">Generated by <a
109
- href="http://ditz.rubyforge.org/">ditz</a>.</p>
110
- </body>
111
- </html>
@@ -1,33 +0,0 @@
1
- <table>
2
- <% issues.sort_by { |i| i.sort_order }.each do |i| %>
3
- <tr>
4
- <td class="issuestatus_<%= i.status %>">
5
- <% if i.closed? %>
6
- <%= i.disposition_string %>
7
- <% else %>
8
- <%= i.status_string %>
9
- <% end %>
10
- </td>
11
- <td class="issuename">
12
- <%= fancy_issue_link_for i %>
13
- <%= i.bug? ? '(bug)' : '' %>
14
- </td>
15
- <% if show_release %>
16
- <td class="issuerelease">
17
- <% if i.release %>
18
- <% r = project.release_for i.release %>
19
- in <%= link_to r, "release #{i.release}" %>
20
- (<%= r.status %>)
21
- <% else %>
22
- <% end %>
23
- </td>
24
- <% end %>
25
- <% if show_component %>
26
- <td class="issuecomponent">
27
- component <%= link_to project.component_for(i.component), i.component %>
28
- </td>
29
- <% end %>
30
- </tr>
31
- <% end %>
32
- </table>
33
-
@@ -1,208 +0,0 @@
1
- require 'yaml'
2
- require 'sha1'
3
- require "lowline"; include Lowline
4
- require "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 Ditz
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 name do
94
- return @values[name] if @values.member?(name)
95
- @values[name] = deserialized_form_of name, @serialized_values[name]
96
- end
97
- end
98
-
99
- def self.field_names; @fields.map { |name, opts| name } end
100
- class << self
101
- attr_reader :fields, :values, :serialized_values
102
- end
103
-
104
- def self.changes_are_logged
105
- define_method(:changes_are_logged?) { true }
106
- field :log_events, :multi => true, :ask => false
107
- end
108
-
109
- def self.from fn
110
- returning YAML::load_file(fn) do |o|
111
- raise ModelError, "error loading from yaml file #{fn.inspect}: expected a #{self}, got a #{o.class}" unless o.class == self
112
- o.pathname = fn if o.respond_to? :pathname=
113
- end
114
- end
115
-
116
- def to_s
117
- "<#{self.class.name}: " + self.class.field_names.map { |f| "#{f}: " + (@values[f].to_s || @serialized_values[f]).inspect }.join(", ") + ">"
118
- end
119
-
120
- def inspect; to_s end
121
-
122
- ## depth-first search on all reachable ModelObjects. fuck yeah.
123
- def each_modelobject
124
- seen = {}
125
- to_see = [self]
126
- until to_see.empty?
127
- cur = to_see.pop
128
- seen[cur] = true
129
- yield cur
130
- cur.class.field_names.each do |f|
131
- val = cur.send(f)
132
- next if seen[val]
133
- if val.is_a?(ModelObject)
134
- to_see.push val
135
- elsif val.is_a?(Array)
136
- to_see += val.select { |v| v.is_a?(ModelObject) }
137
- end
138
- end
139
- end
140
- end
141
-
142
- def save! fn
143
- #FileUtils.mv fn, "#{fn}~", :force => true rescue nil
144
- File.open(fn, "w") { |f| f.puts to_yaml }
145
- self
146
- end
147
-
148
- def to_yaml opts={}
149
- ret = YAML::quick_emit(object_id, opts) do |out|
150
- out.map(taguri, nil) do |map|
151
- self.class.fields.each do |f, fops|
152
- v = if @serialized_values.member?(f)
153
- @serialized_values[f]
154
- else
155
- @serialized_values[f] = serialized_form_of f, @values[f]
156
- end
157
-
158
- map.add f.to_s, v
159
- end
160
- end
161
- end
162
- ret.decode
163
- end
164
-
165
- def log what, who, comment
166
- add_log_event([Time.now, who, what, comment || ""])
167
- self
168
- end
169
-
170
- def changed?; @changed ||= false end
171
- def changed!; @changed = true end
172
- def unchanged!; @changed = false end
173
-
174
- def self.create_interactively opts={}
175
- o = self.new
176
- args = opts[:args] || []
177
- @fields.each do |name, field_opts|
178
- val = if opts[:with] && opts[:with][name]
179
- opts[:with][name]
180
- elsif field_opts[:generator].is_a? Proc
181
- field_opts[:generator].call(*args)
182
- elsif field_opts[:generator]
183
- o.send field_opts[:generator], *args
184
- elsif field_opts[:ask] == false # nil counts as true here
185
- field_opts[:default] || (field_opts[:multi] ? [] : nil)
186
- else
187
- q = field_opts[:prompt] || name.to_s.capitalize
188
- if field_opts[:multiline]
189
- ask_multiline q
190
- else
191
- default = if field_opts[:default_generator].is_a? Proc
192
- field_opts[:default_generator].call(*args)
193
- elsif field_opts[:default_generator]
194
- o.send field_opts[:default_generator], *args
195
- elsif field_opts[:default]
196
- field_opts[:default]
197
- end
198
-
199
- ask q, :default => default
200
- end
201
- end
202
- o.send("#{name}=", val)
203
- end
204
- o
205
- end
206
- end
207
-
208
- end