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.
@@ -0,0 +1,83 @@
1
+ ## git-sync ditz plugin
2
+ ##
3
+ ## This plugin is useful for when you want synchronized, non-distributed issue
4
+ ## coordination with other developers, and you're using git. It allows you to
5
+ ## synchronize issue updates with other developers by using the 'ditz sync'
6
+ ## command, which does all the git work of sending and receiving issue change
7
+ ## for you. However, you have to set things up in a very specific way for this
8
+ ## to work:
9
+ ##
10
+ ## 1. Your ditz state must be on a separate branch. I recommend calling it
11
+ ## 'bugs'. Create this branch, do a ditz init, and push it to the remote
12
+ ## repo. (This means you won't be able to mingle issue change and code
13
+ ## change in the same commits. If you care.)
14
+ ## 2. Make a checkout of the bugs branch in a separate directory, but NOT in
15
+ ## your code checkout. If you're developing in a directory called "project",
16
+ ## I recommend making a ../project-bugs/ directory, cloning the repo there
17
+ ## as well, and keeping that directory checked out to the 'bugs' branch.
18
+ ## (There are various complicated things you can do to make that directory
19
+ ## share git objects with your code directory, but I wouldn't bother unless
20
+ ## you really care about disk space. Just make it an independent clone.)
21
+ ## 3. Set that directory as your issue-dir in your .ditz-config file in your
22
+ ## code checkout directory. (This file should be in .gitignore, btw.)
23
+ ## 4. Run 'ditz reconfigure' and fill in the local branch name, remote
24
+ ## branch name, and remote repo for the issue tracking branch.
25
+ ##
26
+ ## Once that's set up, 'ditz sync' will change to the bugs checkout dir, bundle
27
+ ## up any changes you've made to issue status, push them to the remote repo,
28
+ ## and pull any new changes in too. All ditz commands will read from your bugs
29
+ ## directory, so you should be able to use ditz without caring about where
30
+ ## things are anymore.
31
+ ##
32
+ ## This complicated setup is necessary to avoid accidentally mingling code
33
+ ## change and issue change. With this setup, issue change is synchronized,
34
+ ## but how you synchronize code is still up to you.
35
+ ##
36
+ ## Usage:
37
+ ## 0. read all the above text very carefully
38
+ ## 1. add a line "- git-sync" to the .ditz-plugins file in the project
39
+ ## root
40
+ ## 2. run 'ditz reconfigure' and answer its questions
41
+ ## 3. run 'ditz sync' with abandon
42
+
43
+ module Ditz
44
+
45
+ class Config
46
+ field :git_sync_local_branch, :prompt => "Local bugs branch name for ditz sync", :default => "bugs"
47
+ field :git_sync_remote_branch, :prompt => "Remote bugs branch name for ditz sync", :default => "bugs"
48
+ field :git_sync_remote_repo, :prompt => "Remote bugs repo name for ditz sync", :default => "origin"
49
+ end
50
+
51
+ class Operator
52
+ operation :sync, "Sync the repo containing ditz via git" do
53
+ opt :dry_run, "Dry run: print the commants, but don't execute them", :short => "n"
54
+ end
55
+ def sync project, config, opts
56
+ unless config.git_sync_local_branch
57
+ $stderr.puts "Please run ditz reconfigure and set the local and remote branch names"
58
+ return
59
+ end
60
+
61
+ Dir.chdir $project_root
62
+ puts "[in #{$project_root}]"
63
+ sh "git add *.yaml", :force => true, :fake => opts[:dry_run]
64
+ sh "git commit -m 'issue updates'", :force => true, :fake => opts[:dry_run]
65
+ sh "git pull", :fake => opts[:dry_run]
66
+ sh "git push #{config.git_sync_remote_repo} #{config.git_sync_local_branch}:#{config.git_sync_remote_branch}", :fake => opts[:dry_run]
67
+ puts
68
+ puts "Ditz issue state synchronized."
69
+ end
70
+
71
+ private
72
+
73
+ def sh s, opts={}
74
+ puts "+ #{s}"
75
+ return if opts[:fake]
76
+ unless system(s) || opts[:force]
77
+ $stderr.puts "non-zero (#{$?.exitstatus}) exit code: #{s}"
78
+ exit(-1)
79
+ end
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,153 @@
1
+ ## git ditz plugin
2
+ ##
3
+ ## This plugin allows issues to be associated with git commits and git
4
+ ## branches. Git commits can be easily tagged with a ditz issue with the 'ditz
5
+ ## commit' command, and both 'ditz show' and the ditz HTML output will then
6
+ ## contain a list of associated commits for each issue.
7
+ ##
8
+ ## Issues can also be assigned a single git feature branch. In this case, all
9
+ ## commits on that branch will listed as commits for that issue. This
10
+ ## particular feature is fairly rudimentary, however---it assumes the reference
11
+ ## point is the 'master' branch, and once the feature branch is merged back
12
+ ## into master, the list of commits disappears.
13
+ ##
14
+ ## Two configuration variables are added, which, when specified, are used to
15
+ ## construct HTML links for the git commit id and branch names in the generated
16
+ ## HTML output.
17
+ ##
18
+ ## Commands added:
19
+ ## ditz set-branch: set the git branch of an issue
20
+ ## ditz commit: run git-commit, and insert the issue id into the commit
21
+ ## message.
22
+ ##
23
+ ## Usage:
24
+ ## 1. add a line "- git" to the .ditz-plugins file in the project root
25
+ ## 2. run ditz reconfigure, and enter the URL prefixes, if any, from
26
+ ## which to create commit and branch links.
27
+ ## 3. use 'ditz commit' with abandon.
28
+
29
+ require 'time'
30
+
31
+ module Ditz
32
+ class Issue
33
+ field :git_branch, :ask => false
34
+
35
+ def git_commits
36
+ return @git_commits if @git_commits
37
+
38
+ filters = ["--grep=\"Ditz-issue: #{id}\""]
39
+ filters << "master..#{git_branch}" if git_branch
40
+
41
+ output = filters.map do |f|
42
+ `git log --pretty=format:\"%aD\t%an <%ae>\t%h\t%s\" #{f} 2> /dev/null`
43
+ end.join
44
+
45
+ @git_commits = output.split(/\n/).map { |l| l.split("\t") }.
46
+ map { |date, email, hash, msg| [Time.parse(date).utc, email, hash, msg] }
47
+ end
48
+ end
49
+
50
+ class Config
51
+ field :git_commit_url_prefix, :prompt => "URL prefix to link git commits to", :default => ""
52
+ field :git_branch_url_prefix, :prompt => "URL prefix to link git branches to", :default => ""
53
+ end
54
+
55
+ class ScreenView
56
+ add_to_view :issue_summary do |issue, config|
57
+ " Git branch: #{issue.git_branch || 'none'}\n"
58
+ end
59
+
60
+ add_to_view :issue_details do |issue, config|
61
+ commits = issue.git_commits[0...5]
62
+ next if commits.empty?
63
+ "Recent commits:\n" + commits.map do |date, email, hash, msg|
64
+ "- #{msg} [#{hash}] (#{email.shortened_email}, #{date.ago} ago)\n"
65
+ end.join + "\n"
66
+ end
67
+ end
68
+
69
+ class HtmlView
70
+ add_to_view :issue_summary do |issue, config|
71
+ next unless issue.git_branch
72
+ [<<EOS, { :issue => issue, :url_prefix => config.git_branch_url_prefix }]
73
+ <tr>
74
+ <td class='attrname'>Git branch:</td>
75
+ <td class='attrval'><%= url_prefix && !url_prefix.blank? ? link_to([url_prefix, issue.git_branch].join, issue.git_branch) : h(issue.git_branch) %></td>
76
+ </tr>
77
+ EOS
78
+ end
79
+
80
+ add_to_view :issue_details do |issue, config|
81
+ commits = issue.git_commits
82
+ next if commits.empty?
83
+
84
+ [<<EOS, { :commits => commits, :url_prefix => config.git_commit_url_prefix }]
85
+ <h2>Commits for this issue</h2>
86
+ <table class="log">
87
+ <% commits.each_with_index do |(time, who, hash, msg), i| %>
88
+ <tr class="<%= i % 2 == 0 ? "even-row" : "odd-row" %>">
89
+ <td class="time"><%=t time %></td>
90
+ <td class="person"><%=obscured_email who %></td>
91
+ <td class="message"><%=h msg %> [<%= url_prefix && !url_prefix.blank? ? link_to([url_prefix, hash].join, hash) : hash %>]</td>
92
+ </tr>
93
+ <tr><td></td></tr>
94
+ <% end %>
95
+ </table>
96
+ EOS
97
+ end
98
+ end
99
+
100
+ class Operator
101
+ operation :set_branch, "Set the git feature branch of an issue", :issue, :maybe_string
102
+ def set_branch project, config, issue, maybe_string
103
+ puts "Issue #{issue.name} currently " + if issue.git_branch
104
+ "assigned to git branch #{issue.git_branch.inspect}."
105
+ else
106
+ "not assigned to any git branch."
107
+ end
108
+
109
+ branch = maybe_string || ask("Git feature branch name:")
110
+ return unless branch
111
+
112
+ if branch == issue.git_branch
113
+ raise Error, "issue #{issue.name} already assigned to branch #{issue.git_branch.inspect}"
114
+ end
115
+
116
+ puts "Assigning to branch #{branch.inspect}."
117
+ issue.git_branch = branch
118
+ end
119
+
120
+ operation :commit, "Runs git-commit and auto-fills the issue name in the commit message", :issue do
121
+ opt :all, "commit all changed files", :short => "-a", :default => false
122
+ opt :verbose, "show diff between HEAD and what would be committed", \
123
+ :short => "-v", :default => false
124
+ opt :message, "Use the given <s> as the commit message.", \
125
+ :short => "-m", :type => :string
126
+ opt :edit, "Further edit the message, even if --message is given.", :short => "-e", :default => false
127
+ end
128
+
129
+ def commit project, config, opts, issue
130
+ opts[:edit] = true if opts[:message].nil?
131
+
132
+ args = {
133
+ :verbose => "--verbose",
134
+ :all => "--all",
135
+ :edit => "--edit",
136
+ }.map { |k, v| opts[k] ? v : "" }.join(" ")
137
+
138
+ comment = "# #{issue.name}: #{issue.title}"
139
+ tag = "Ditz-issue: #{issue.id}"
140
+ message = if opts[:message] && !opts[:edit]
141
+ "#{opts[:message]}\n\n#{tag}"
142
+ elsif opts[:message] && opts[:edit]
143
+ "#{opts[:message]}\n\n#{tag}\n\n#{comment}"
144
+ else
145
+ "#{comment}\n\n#{tag}"
146
+ end
147
+
148
+ message = message.gsub("\"", "\\\"")
149
+ exec "git commit #{args} --message=\"#{message}\""
150
+ end
151
+ end
152
+
153
+ end
@@ -0,0 +1,193 @@
1
+ ## issue-claiming ditz plugin
2
+ ##
3
+ ## This plugin allows people to claim issues. This is useful for avoiding
4
+ ## duplication of work---you can check to see if someone's claimed an
5
+ ## issue before starting to work on it, and you can let people know what
6
+ ## you're working on.
7
+ ##
8
+ ## Commands added:
9
+ ## ditz claim: claim an issue for yourself or a dev specified in project.yaml
10
+ ## ditz unclaim: unclaim a claimed issue
11
+ ## ditz mine: show all issues claimed by you
12
+ ## ditz claimed: show all claimed issues, by developer
13
+ ## ditz unclaimed: show all unclaimed issues
14
+ ##
15
+ ## Usage:
16
+ ## 1. add a line "- issue-claiming" to the .ditz-plugins file in the project
17
+ ## root
18
+ ## 2. (optional:) add a 'devs' key to project.yaml, e.g:
19
+ ## devs:
20
+ ## :roy: Roy Baty <roy@marsproject.com>
21
+ ## :pris: Pris Stratton <pris@marsproject.com>
22
+
23
+ ## 3. use the above commands to abandon
24
+
25
+ module Ditz
26
+
27
+ class Project
28
+ field :devs, :prompt => "Hash of developer nicknames (as symbols) to email addresses",
29
+ :default => {:roy => "Roy Baty <roy@marsproject.com>"}
30
+ end
31
+
32
+ class Issue
33
+ field :claimer, :ask => false
34
+
35
+ def claim who, comment, force=false
36
+ raise Error, "already claimed by #{claimer}" if claimer && !force
37
+ log "issue claimed", who, comment
38
+ self.claimer = who
39
+ end
40
+
41
+ def unclaim who, comment, force=false
42
+ raise Error, "not claimed" unless claimer
43
+ raise Error, "can only be unclaimed by #{claimer}" unless claimer == who || force
44
+ if claimer == who
45
+ log "issue unclaimed", who, comment
46
+ else
47
+ log "unclaimed from #{claimer}", who, comment
48
+ end
49
+ self.claimer = nil
50
+ end
51
+
52
+ def claimed?; claimer end
53
+ def unclaimed?; !claimed? end
54
+ end
55
+
56
+ class ScreenView
57
+ add_to_view :issue_summary do |issue, config|
58
+ " Claimed by: #{issue.claimer || 'none'}\n"
59
+ end
60
+ end
61
+
62
+ class HtmlView
63
+ add_to_view :issue_summary do |issue, config|
64
+ next unless issue.claimer
65
+ [<<EOS, { :issue => issue }]
66
+ <tr>
67
+ <td class='attrname'>Claimed by:</td>
68
+ <td class='attrval'><%= h(issue.claimer) %></td>
69
+ </tr>
70
+ EOS
71
+ end
72
+ end
73
+
74
+ class Operator
75
+ alias :__issue_claiming_start :start
76
+ def start project, config, opts, issue
77
+ if issue.claimed? && issue.claimer != config.user
78
+ raise Error, "issue #{issue.name} claimed by #{issue.claimer}"
79
+ else
80
+ __issue_claiming_start project, config, opts, issue
81
+ end
82
+ end
83
+
84
+ alias :__issue_claiming_stop :stop
85
+ def stop project, config, opts, issue
86
+ if issue.claimed? && issue.claimer != config.user
87
+ raise Error, "issue #{issue.name} claimed by #{issue.claimer}"
88
+ else
89
+ __issue_claiming_stop project, config, opts, issue
90
+ end
91
+ end
92
+
93
+ alias :__issue_claiming_close :close
94
+ def close project, config, opts, issue
95
+ if issue.claimed? && issue.claimer != config.user
96
+ raise Error, "issue #{issue.name} claimed by #{issue.claimer}"
97
+ else
98
+ __issue_claiming_close project, config, opts, issue
99
+ end
100
+ end
101
+
102
+ operation :claim, "Claim an issue for yourself", :issue, :maybe_dev do
103
+ opt :force, "Claim this issue even if someone else has claimed it", :default => false
104
+ end
105
+ def claim project, config, opts, issue, dev = nil
106
+ if dev
107
+ dev_full_email = project.devs ? project.devs[dev.to_sym] : nil
108
+ raise Error, "no nickname :#{dev} has been defined in project.yaml" unless dev_full_email
109
+ end
110
+ dev_full_email ||= config.user
111
+ puts "Claiming issue #{issue.name}: #{issue.title} for #{dev_full_email}."
112
+ comment = ask_multiline_or_editor "Comments" unless $opts[:no_comment]
113
+ issue.claim dev_full_email, comment, opts[:force]
114
+ puts "Issue #{issue.name} marked as claimed by #{dev_full_email}"
115
+ end
116
+
117
+ operation :unclaim, "Unclaim a claimed issue", :issue do
118
+ opt :force, "Unclaim this issue even if it's claimed by someone else", :default => false
119
+ end
120
+ def unclaim project, config, opts, issue
121
+ puts "Unclaiming issue #{issue.name}: #{issue.title}."
122
+ comment = ask_multiline_or_editor "Comments" unless $opts[:no_comment]
123
+ issue.unclaim config.user, comment, opts[:force]
124
+ puts "Issue #{issue.name} marked as unclaimed."
125
+ end
126
+
127
+ operation :mine, "Show all issues claimed by you", :maybe_release do
128
+ opt :all, "Show all issues, not just open ones"
129
+ end
130
+ def mine project, config, opts, releases
131
+ releases ||= project.unreleased_releases + [:unassigned]
132
+ releases = [*releases]
133
+
134
+ issues = project.issues.select do |i|
135
+ r = project.release_for(i.release) || :unassigned
136
+ releases.member?(r) && i.claimer == config.user && (opts[:all] || i.open?)
137
+ end
138
+ if issues.empty?
139
+ puts "No issues."
140
+ else
141
+ run_pager config
142
+ print_todo_list_by_release_for project, issues
143
+ end
144
+ end
145
+
146
+ operation :claimed, "Show claimed issues by claimer", :maybe_release do
147
+ opt :all, "Show all issues, not just open ones"
148
+ end
149
+ def claimed project, config, opts, releases
150
+ releases ||= project.unreleased_releases + [:unassigned]
151
+ releases = [*releases]
152
+
153
+ issues = project.issues.inject({}) do |h, i|
154
+ r = project.release_for(i.release) || :unassigned
155
+ if i.claimed? && (opts[:all] || i.open?) && releases.member?(r)
156
+ (h[i.claimer] ||= []) << i
157
+ end
158
+ h
159
+ end
160
+
161
+ if issues.empty?
162
+ puts "No issues."
163
+ else
164
+ run_pager config
165
+ issues.keys.sort.each do |c|
166
+ puts "#{c}:"
167
+ puts todo_list_for(issues[c], :show_release => true)
168
+ puts
169
+ end
170
+ end
171
+ end
172
+
173
+ operation :unclaimed, "Show all unclaimed issues", :maybe_release do
174
+ opt :all, "Show all issues, not just open ones"
175
+ end
176
+ def unclaimed project, config, opts, releases
177
+ releases ||= project.unreleased_releases + [:unassigned]
178
+ releases = [*releases]
179
+
180
+ issues = project.issues.select do |i|
181
+ r = project.release_for(i.release) || :unassigned
182
+ releases.member?(r) && i.claimer.nil? && (opts[:all] || i.open?)
183
+ end
184
+ if issues.empty?
185
+ puts "No issues."
186
+ else
187
+ run_pager config
188
+ print_todo_list_by_release_for project, issues
189
+ end
190
+ end
191
+ end
192
+
193
+ end
@@ -0,0 +1,170 @@
1
+ ## issue-labeling ditz plugin
2
+ ##
3
+ ## This plugin allows label issues. This can replace the issue component
4
+ ## and/or issue types (bug,feature,task), by providing a more flexible
5
+ ## to organize your issues.
6
+ ##
7
+ ## Commands added:
8
+ ## ditz new_label [label]: create a new label for the project
9
+ ## ditz label <issue> <labels>: label an issue with some labels
10
+ ## ditz unlabel <issue> [labels]: remove some label(s) of an issue
11
+ ## ditz labeled <labels> [release]: show all issues with these labels
12
+ ##
13
+ ## Usage:
14
+ ## 1. add a line "- issue-labeling" to the .ditz-plugins file in the project
15
+ ## root
16
+ ## 2. use the above commands to abandon
17
+
18
+ # TODO:
19
+ # * extend the HTML view to have per-labels listings
20
+ # * allow for more compact way to type them (completion, prefixes...)
21
+
22
+ module Ditz
23
+
24
+ class Label < ModelObject
25
+ include Comparable
26
+ field :name
27
+
28
+ def name_prefix; @name.gsub(/\s+/, "-").downcase end
29
+
30
+ def <=> x ; name <=> x.name end
31
+ def == x ; name == x.name end
32
+
33
+ end # class Label
34
+
35
+ class Project
36
+ field :labels, :multi => true, :interactive_generator => :get_labels
37
+
38
+ def get_labels
39
+ lab_names = ask_for_many("labels")
40
+ ([name] + lab_names).uniq.map { |n| Label.create_interactively :with => { :name => n } }
41
+ end
42
+
43
+ def label_for label_name
44
+ labels.find { |i| i.name == label_name }
45
+ end
46
+
47
+ def labels_for label_names
48
+ label_names.split(/\s*,\s*/).map do |val|
49
+ label_for(val) or raise Error, "no label with name #{val}"
50
+ end
51
+ end
52
+
53
+ end # class Project
54
+
55
+ class Issue
56
+ field :labels, :multi => true, :interactive_generator => :get_labels
57
+
58
+ def get_labels config, project
59
+ ask_for_selection(project.labels, "label", :name, true).map {|x|x}
60
+ end
61
+
62
+ def apply_labels new_labels, who, comment
63
+ new_labels.each { |l| add_label l }
64
+ log "issue labeled", who, comment
65
+ end
66
+
67
+ def remove_labels labels_to_remove, who, comment
68
+ log "issue unlabeled", who, comment
69
+ if labels_to_remove.nil?
70
+ self.labels = []
71
+ else
72
+ labels_to_remove.each { |l| drop_label l }
73
+ end
74
+ end
75
+
76
+ def labeled? label=nil; (label.nil?)? !labels.empty? : labels.include?(label) end
77
+ def unlabeled? label=nil; !labeled?(label) end
78
+ end
79
+
80
+ class ScreenView
81
+ add_to_view :issue_summary do |issue, config|
82
+ " Labels: #{(issue.labeled?)? issue.labels.map{|l|l.name}.join(', ') : 'none'}\n"
83
+ end
84
+ end
85
+
86
+ class HtmlView
87
+ add_to_view :issue_summary do |issue, config|
88
+ next if issue.unlabeled?
89
+ [<<EOS, { :issue => issue }]
90
+ <tr>
91
+ <td class='attrname'>Labels:</td>
92
+ <td class='attrval'><%= h(issue.labels.map{|l|l.name}.join(', ')) %></td>
93
+ </tr>
94
+ EOS
95
+ end
96
+ end
97
+
98
+ class Operator
99
+
100
+ operation :new_label, "Create a new label for the project", :maybe_label
101
+ def new_label project, config, maybe_label
102
+ puts "Adding label #{maybe_label}." if maybe_label
103
+ label = Label.create_interactively(:args => [project, config], :with => { :name => maybe_label }) or return
104
+ begin
105
+ project.add_label label
106
+ puts "Added label #{label.name}."
107
+ rescue Ditz::ModelError => e
108
+ puts "Error: Project #{e}"
109
+ end
110
+ end
111
+
112
+ operation :label, "Apply labels to an issue", :issue, :labels
113
+ def label project, config, issue, label_names
114
+ labels = project.labels_for label_names
115
+ puts "Adding labels #{label_names} to issue #{issue.name}: #{issue.title}."
116
+ comment = ask_multiline_or_editor "Comments" unless $opts[:no_comment]
117
+ begin
118
+ issue.apply_labels labels, config.user, comment
119
+ rescue Ditz::ModelError => e
120
+ puts "Error: Issue #{e}"
121
+ return
122
+ end
123
+ show_labels issue
124
+ end
125
+
126
+ operation :unlabel, "Remove some labels of an issue", :issue, :maybe_labels
127
+ def unlabel project, config, issue, label_names
128
+ labels = if label_names
129
+ puts "Removing #{label_names} labels from issue #{issue.name}: #{issue.title}."
130
+ project.labels_for label_names
131
+ else
132
+ puts "Removing labels from issue #{issue.name}: #{issue.title}."
133
+ nil
134
+ end
135
+ comment = ask_multiline_or_editor "Comments" unless $opts[:no_comment]
136
+ issue.remove_labels labels, config.user, comment
137
+ show_labels issue
138
+ end
139
+
140
+ def show_labels issue
141
+ if issue.labeled?
142
+ puts "Issue #{issue.name} is now labeled with #{issue.labels.map{|l|l.name}.join(', ')}"
143
+ else
144
+ puts "Issue #{issue.name} is now unlabeled"
145
+ end
146
+ end
147
+ private :show_labels
148
+
149
+ operation :labeled, "Show labeled issues", :labels, :maybe_release do
150
+ opt :all, "Show all issues, not just open ones"
151
+ end
152
+ def labeled project, config, opts, labels, releases
153
+ releases ||= project.unreleased_releases + [:unassigned]
154
+ releases = [*releases]
155
+ labels = project.labels_for labels
156
+
157
+ issues = project.issues.select do |i|
158
+ r = project.release_for(i.release) || :unassigned
159
+ labels.all? { |l| i.labeled? l } && (opts[:all] || i.open?) && releases.member?(r)
160
+ end
161
+
162
+ if issues.empty?
163
+ puts "No issues."
164
+ else
165
+ print_todo_list_by_release_for project, issues
166
+ end
167
+ end
168
+ end
169
+
170
+ end
data/lib/ditz/util.rb ADDED
@@ -0,0 +1,61 @@
1
+ class Object
2
+ def returning o; yield o; o end # k-combinator
3
+ end
4
+
5
+ module Enumerable
6
+ def count_of(&b); select(&b).size end
7
+ def max_of(&b); map(&b).max end
8
+ def min_of(&b); map(&b).min end
9
+ end
10
+
11
+ class Array
12
+ def uniq_by; inject({}) { |h, o| h[yield(o)] = o; h }.values end
13
+ end
14
+
15
+ class NilClass
16
+ def blank?; true end
17
+ end
18
+
19
+ module Enumerable
20
+ def map_with_index # sigh...
21
+ ret = []
22
+ each_with_index { |e, i| ret << yield(e, i) }
23
+ ret
24
+ end
25
+
26
+ def argfind
27
+ each { |e| x = yield(e); return x if x }
28
+ nil
29
+ end
30
+
31
+ def group_by
32
+ inject({}) do |groups, element|
33
+ (groups[yield(element)] ||= []) << element
34
+ groups
35
+ end
36
+ end if RUBY_VERSION < '1.9'
37
+
38
+ end
39
+
40
+ class Array
41
+ def first_duplicate
42
+ sa = sort
43
+ (1 .. sa.length).argfind { |i| (sa[i] == sa[i - 1]) && sa[i] }
44
+ end
45
+
46
+ def to_h
47
+ Hash[*flatten]
48
+ end
49
+
50
+ def flatten_one_level
51
+ inject([]) do |ret, e|
52
+ case e
53
+ when Array
54
+ ret + e
55
+ else
56
+ ret << e
57
+ end
58
+ end
59
+ end
60
+ end
61
+
data/lib/ditz/view.rb ADDED
@@ -0,0 +1,16 @@
1
+ module Ditz
2
+
3
+ class View
4
+ def self.add_to_view type, &block
5
+ @views ||= {}
6
+ @views[type] ||= []
7
+ @views[type] << block
8
+ end
9
+
10
+ def self.view_additions_for type
11
+ @views ||= {}
12
+ @views[type] || []
13
+ end
14
+ end
15
+
16
+ end