ursm-ditz 0.4

Sign up to get free protection for your applications and to get access to all the features.
data/Changelog ADDED
@@ -0,0 +1,35 @@
1
+ == 0.4 / 2008-07-27
2
+ * bugfix: HOME environment variable now correctly detected on windows
3
+ * hooks loaded from both home directory and project directory
4
+ * added bash shell completion
5
+ * plugin architecture for tighter SCM integration, etc
6
+ * 'ditz grep' should also grep against comments, log messages, etc
7
+ * added man page
8
+ * removed ditz-convert-from-monolith
9
+ * lots of HTML output tweaking
10
+ == 0.3 / 2008-06-04
11
+ * readline support for all text entry
12
+ * hook system. Use ditz -l to see possible hooks.
13
+ * new commands: archive, shortlog, set-component
14
+ * improved commands: log, assign, add-release
15
+ * new issue type: 'tasks'
16
+ * 'ditz' by itself shows the todo list
17
+ * zsh tab completion for subcommands
18
+ * local config can now specify bugs directory location
19
+ * issue name interpolation now on all issue fields
20
+ * bugfix: various HTML generation bugs
21
+ * bugfix: ditz now works from project subdirectories
22
+ * bugfix: removed UNIX-specific environment variable assumptions
23
+ == 0.2 / 2008-04-11
24
+ * bugfix: store each issue in a separate file to avoid false conflicts
25
+ * added per-command help
26
+ * added 'log' command for recent activity
27
+ * added better exception handling---turn into pretty error messages
28
+ * added text-area commands like /edit, /reset, etc
29
+ * all times now stored in UTC
30
+ == 0.1.2 / 2008-04-04
31
+ * bugfix: add_reference very broken
32
+ == 0.1.1 / 2008-04-04
33
+ * bugfix: bugfix/feature question always returns feature
34
+ == 0.1 / 2008-04-02
35
+ * Initial release!
data/README.txt ADDED
@@ -0,0 +1,127 @@
1
+ == ditz
2
+
3
+ by William Morgan <wmorgan-ditz at the masanjin dot nets>
4
+
5
+ http://ditz.rubyforge.org
6
+
7
+ == DESCRIPTION
8
+
9
+ Ditz is a simple, light-weight distributed issue tracker designed to work with
10
+ distributed version control systems like git, darcs, Mercurial, and Bazaar. It
11
+ can also be used with centralized systems like SVN.
12
+
13
+ Ditz maintains an issue database directory on disk, with files written in a
14
+ line-based and human-editable format. This directory can be kept under version
15
+ control, alongside project code.
16
+
17
+ There are several different ways to use ditz:
18
+
19
+ 1. Treat issue change the same as code change: include it as part of commits,
20
+ and merge it with changes from other developers, resolving conflicts in the
21
+ usual manner.
22
+ 2. Keep the issue database in the repository but in a separate branch. Issue
23
+ changes can be managed by your VCS, but is not tied directly to code
24
+ commits.
25
+ 3. Keep the issue database separate and not under VCS at all.
26
+
27
+ Ditz provides a simple, console-based interface for creating and updating the
28
+ issue database file, and some rudimentary static HTML generation capabilities
29
+ for producing world-readable status pages (for a demo, see the ditz ditz page).
30
+ It currently offers no central public method of bug submission.
31
+
32
+ == SYNOPSIS
33
+
34
+ # set up project. creates the bugs.yaml file.
35
+ 1. ditz init
36
+ 2. ditz add-release
37
+
38
+ # add an issue
39
+ 3. ditz add
40
+
41
+ # where am i?
42
+ 4. ditz status
43
+ 5. ditz todo (or simply "ditz")
44
+
45
+ # do work
46
+ 6. write code
47
+ 7. ditz close <issue-id>
48
+ 8. commit
49
+ 9. goto 3
50
+
51
+ # finished!
52
+ 10. ditz release <release-name>
53
+
54
+ == THE DITZ DATA MODEL
55
+
56
+ Ditz includes the bare minimum set of features necessary for open-source
57
+ development. Features like time spent, priority, assignment of tasks to
58
+ developers, due dates, etc. are purposely excluded.
59
+
60
+ A ditz project consists of issues, releases and components.
61
+
62
+ Issues:
63
+ Issues are the fundamental currency of issue tracking. A ditz issue is either
64
+ a feature or a bug, but this distinction doesn't affect anything other than
65
+ how they're displayed.
66
+
67
+ Each issue belongs to exactly one component, and is part of zero or one
68
+ releases.
69
+
70
+ Each issues has an exportable id, in the form of 40 random hex characters.
71
+ This id is "guaranteed" to be unique across all possible issues and
72
+ developers, present and future. Issue ids are typically not exposed to the
73
+ user.
74
+
75
+ Issues also have a non-exportable name, which is short and human-readable.
76
+ All ditz commands use issue names instead of issue ids. Issue ids may change
77
+ in certain circumstances, specifically after a "ditz drop" command.
78
+
79
+ Components:
80
+ There is always one "general" component, named after the project itself. In
81
+ the simplest case, this is the only component, and the user is never bothered
82
+ with the question of which component to assign an issue to.
83
+
84
+ Components simply provide a way of organizing issues, and have no real
85
+ functionality. Issues are assigned names derived form the component they're
86
+ assigned to.
87
+
88
+ Releases:
89
+ A release is the primary grouping mechanism for issues. Status commands like
90
+ "ditz status" and "ditz todo" always group issues by release. When a release
91
+ is 100% complete, it can be marked as released, in which case the associated
92
+ issues will cease appearing in ditz status and todo messages.
93
+
94
+ == FUTURE WORK
95
+
96
+ In future releases, Ditz will have a plugin architecture to allow tighter
97
+ integration with specific SCMs and developer communication channels. (See
98
+ http://ditz.rubyforge.org/ditz/issue-0704dafe4aef96279364013aba177a0971d425cb.html)
99
+
100
+ == LEARNING MORE
101
+
102
+ * ditz help
103
+ * find $DITZ_INSTALL_DIR -type f | xargs cat
104
+
105
+ == REQUIREMENTS
106
+
107
+ * trollop >= 1.8.2
108
+
109
+ == INSTALLATION
110
+
111
+ Download tarballs from http://rubyforge.org/projects/ditz/, or command your
112
+ computer to "gem install ditz".
113
+
114
+ == LICENSE
115
+
116
+ Copyright (c) 2008 William Morgan.
117
+
118
+ This program is free software: you can redistribute it and/or modify
119
+ it under the terms of the GNU General Public License as published by
120
+ the Free Software Foundation, either version 3 of the License, or
121
+ (at your option) any later version.
122
+
123
+ This program is distributed in the hope that it will be useful,
124
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
125
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
126
+ GNU General Public License for more details.
127
+
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+
4
+ $:.unshift "lib"
5
+ require 'ditz'
6
+
7
+ class Hoe
8
+ def extra_deps; @extra_deps.reject { |x| Array(x).first == "hoe" } end
9
+ end # thanks to "Mike H"
10
+
11
+ Hoe.new('ditz', Ditz::VERSION) do |p|
12
+ p.rubyforge_name = 'ditz'
13
+ p.author = "William Morgan"
14
+ p.summary = "A simple issue tracker designed to integrate well with distributed version control systems like git and darcs. State is saved to a YAML file kept under version control, allowing issues to be closed/added/modified as part of a commit."
15
+
16
+ p.description = p.paragraphs_of('README.txt', 4..11).join("\n\n").gsub(/== SYNOPSIS/, "Synopsis:")
17
+ p.url = "http://ditz.rubyforge.org"
18
+ p.changes = p.paragraphs_of('Changelog', 0..0).join("\n\n")
19
+ p.email = "wmorgan-ditz@masanjin.net"
20
+ end
21
+
22
+ WWW_FILES = FileList["www/*"] + %w(README.txt)
23
+
24
+ task :upload_webpage => WWW_FILES do |t|
25
+ sh "rsync -essh -cavz #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
26
+ end
27
+
28
+ task :upload_report do |t|
29
+ sh "ruby -Ilib bin/ditz html ditz"
30
+ sh "rsync -essh -cavz ditz wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
31
+ end
32
+
33
+ # vim: syntax=ruby
data/ReleaseNotes ADDED
@@ -0,0 +1,50 @@
1
+ 0.4
2
+ ---
3
+ - Command-line completion scripts are now included for bash and zsh. To
4
+ activate, source the relevant file in $GEMDIR/ditz-0.4/contrib/completion/.
5
+
6
+ - Hooks can now be set on a per-project basis. Make a .ditz/hooks directory in
7
+ your project root and place them there. These will be loaded after any
8
+ hooks in ~/.ditz/hooks, so they can override or simply supplement.
9
+
10
+ - The plugin system is done. There's currently one plugin, for git integration.
11
+ To enable it, add the line "- git" in a .ditz-plugins file in your project
12
+ root. The git plugin currently has the following features:
13
+
14
+ - Issues can have a git branch assigned to them with "ditz set-branch".
15
+ - Git commit messages can have a Ditz-issue: header auto-filled if you
16
+ commit with "ditz commit <issue>" (i.e. instead of git commit).
17
+ - In both HTML and screen output, commits from the assigned branch, and
18
+ commits with the corresponding Ditz-issue: header in the log message,
19
+ will be listed for each issue.
20
+
21
+ Note that the plugin system is independent of the hook system. In order
22
+ to auto-add ditz files to the git index upon modification, you must set
23
+ up hooks. Example hooks for git are at:
24
+ http://hackety.org/2008/06/26/gitHooksForDitz.html
25
+
26
+ Also note that as soon as a feature branch is merged back into master, ditz
27
+ loses the ability to distinguish its commits. So the Ditz-issue: approach
28
+ is probably better if you want a long-term record.
29
+
30
+ 0.3
31
+ ---
32
+ Ditz now works from project subdirectories, and you can have a .ditz-config in
33
+ the project root for project-specific configuration. (This is not merged with
34
+ the global config, so this file overrides everything in ~/.ditz-config.)
35
+
36
+ You can specify an :issue_dir key in this file, which can be a relative path to
37
+ the directory containing project.yaml. So if you want to rename that directory,
38
+ or keep it somewhere else, now you can.
39
+
40
+ There's also a new hook system for plugging in your own code. Run ditz -l to
41
+ see a list of available hooks.
42
+
43
+ 0.2
44
+ ---
45
+
46
+ In ditz 0.2, we store issues per file. This avoids many unnecessary conflicts
47
+ that occur in the single-file case.
48
+
49
+ To upgrade your bugs.yaml to a bugs/ directory, you must run
50
+ ditz-convert-from-monolith.
data/bin/ditz ADDED
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/ruby1.8
2
+
3
+ ## requires are split in two for efficiency reasons: ditz should be really
4
+ ## fast when using it for completion.
5
+ $KCODE = "u"
6
+
7
+ require 'operator'
8
+ op = Ditz::Operator.new
9
+
10
+ ## a secret option for shell completion
11
+ if ARGV.include? '--commands'
12
+ puts op.class.operations.map { |name, _| name }
13
+ exit 0
14
+ end
15
+
16
+ require 'rubygems' rescue LoadError nil
17
+ require 'fileutils'
18
+ require 'pathname'
19
+ require 'trollop'; include Trollop
20
+ require "ditz"
21
+ require "vendor/yaml_waml"
22
+
23
+ PROJECT_FN = "project.yaml"
24
+ CONFIG_FN = ".ditz-config"
25
+ PLUGIN_FN = ".ditz-plugins"
26
+
27
+ config_dir = Ditz::find_dir_containing CONFIG_FN
28
+ plugin_dir = Ditz::find_dir_containing PLUGIN_FN
29
+
30
+ $opts = options do
31
+ version "ditz #{Ditz::VERSION}"
32
+ opt :issue_dir, "Issue database dir", :default => "bugs"
33
+ opt :config_file, "Configuration file", :default => File.join(config_dir || ".", CONFIG_FN)
34
+ opt :plugins_file, "Plugins file", :default => File.join(plugin_dir || ".", PLUGIN_FN)
35
+ opt :verbose, "Verbose output", :default => false
36
+ opt :no_comment, "Skip asking for a comment", :default => false
37
+ opt :list_hooks, "List all hooks and descriptions, and quit.", :short => 'l', :default => false
38
+ stop_on_unknown
39
+ end
40
+
41
+ Ditz::HookManager.register :startup, <<EOS
42
+ Executes at startup
43
+
44
+ Variables: project, config
45
+ No return value.
46
+ EOS
47
+
48
+ Ditz::HookManager.register :after_add, <<EOS
49
+ Executes before terminating if new issue files has been created.
50
+ Basically you want to instruct your SCM that these files has
51
+ been added.
52
+
53
+ Variables: project, config, issues
54
+ No return value.
55
+ EOS
56
+
57
+ Ditz::HookManager.register :after_delete, <<EOS
58
+ Executes before terminating if new issue files has been deleted.
59
+ Basically you want to instruct your SCM that these files has
60
+ been deleted.
61
+
62
+ Variables: project, config, issues
63
+ No return value.
64
+ EOS
65
+
66
+ Ditz::HookManager.register :after_update, <<EOS
67
+ Executes before terminating if new issue files has been updated.
68
+ You may want to instruct your SCM about these changes.
69
+ Note that new issues are not considered updated.
70
+
71
+ Variables: project, config, issues
72
+ No return value.
73
+ EOS
74
+
75
+ if $opts[:list_hooks]
76
+ Ditz::HookManager.print_hooks
77
+ exit 0
78
+ end
79
+
80
+ plugins = begin
81
+ Ditz::debug "loading plugins from #{$opts[:plugins_file]}"
82
+ YAML::load_file$opts[:plugins_file]
83
+ rescue SystemCallError => e
84
+ Ditz::debug "can't load plugins file: #{e.message}"
85
+ []
86
+ end
87
+
88
+ plugins.each do |p|
89
+ fn = Ditz::find_ditz_file "plugins/#{p}.rb"
90
+ Ditz::debug "loading plugin #{p.inspect} from #{fn}"
91
+ load fn
92
+ end
93
+
94
+ config = begin
95
+ Ditz::debug "loading config from #{$opts[:config_file]}"
96
+ Ditz::Config.from $opts[:config_file]
97
+ rescue SystemCallError => e
98
+ if ARGV.member? "<options>"
99
+ ## special case here. if we're asking for tab completion, and the config
100
+ ## file doesn't exist, don't do the interactive building. just make a
101
+ ## fake empty one and carry on.
102
+ Ditz::Config.new
103
+ else
104
+ puts <<EOS
105
+ I wasn't able to find a configuration file #{$opts[:config_file]}.
106
+ We'll set it up right now.
107
+ EOS
108
+ Ditz::Config.create_interactively.save! $opts[:config_file]
109
+ end
110
+ end
111
+
112
+ cmd = ARGV.shift || "todo"
113
+ issue_dir = Pathname.new(config.issue_dir || $opts[:issue_dir])
114
+
115
+ case cmd # some special commands not handled by Ditz::Operator
116
+ when "init"
117
+ die "#{issue_dir} directory already exists" if issue_dir.exist?
118
+ issue_dir.mkdir
119
+ fn = issue_dir + PROJECT_FN
120
+ project = op.init
121
+ project.save! fn
122
+ puts "Ok, #{issue_dir} directory created successfully."
123
+ exit
124
+ when "help"
125
+ op.do "help", nil, nil, ARGV
126
+ exit
127
+ end
128
+
129
+ project_root = Ditz::find_dir_containing(issue_dir + PROJECT_FN)
130
+ die "No #{issue_dir} directory---use 'ditz init' to initialize" unless project_root
131
+ project_root += issue_dir
132
+
133
+ project = begin
134
+ fn = project_root + PROJECT_FN
135
+ Ditz::debug "loading project from #{fn}"
136
+ project = Ditz::Project.from fn
137
+
138
+ fn = project_root + "issue-*.yaml"
139
+ Ditz::debug "loading issues from #{fn}"
140
+ project.issues = Dir[fn].map { |fn| Ditz::Issue.from fn }
141
+ Ditz::debug "found #{project.issues.size} issues"
142
+ project
143
+ rescue SystemCallError, Ditz::Project::Error => e
144
+ die "#{e.message} (use 'init' to initialize)"
145
+ end
146
+
147
+ project.validate!
148
+ project.issues.each { |p| p.project = project}
149
+ project.assign_issue_names!
150
+
151
+ unless op.has_operation? cmd
152
+ die "no such command: #{cmd}"
153
+ end
154
+
155
+ Ditz::HookManager.run :startup, project, config
156
+
157
+ Ditz::debug "executing command #{cmd}"
158
+ begin
159
+ op.do cmd, project, config, ARGV
160
+ rescue Ditz::Operator::Error, Ditz::Release::Error, Ditz::Project::Error, Ditz::Issue::Error => e
161
+ $stderr.puts "Error: #{e.message}"
162
+ exit(-1)
163
+ rescue Errno::EPIPE, Interrupt
164
+ puts
165
+ exit 1
166
+ end
167
+
168
+ ## save project.yaml
169
+ dirty = project.each_modelobject { |o| break true if o.changed? } || false
170
+ if dirty
171
+ fn = project_root + PROJECT_FN
172
+ Ditz::debug "project is dirty, saving #{fn}"
173
+ project.save! fn
174
+ end
175
+
176
+ ## project issues are not model fields proper, so they must be
177
+ ## saved independently.
178
+ changed_issues = project.issues.select { |i| i.changed? }
179
+ changed_issues.each do |i|
180
+ i.pathname ||= (project_root + "issue-#{i.id}.yaml")
181
+ i.project ||= project # hack: not set on new issues
182
+ Ditz::debug "issue #{i.name} is dirty, saving #{i.pathname}"
183
+ i.save! i.pathname
184
+ end
185
+
186
+ project.deleted_issues.each do |i|
187
+ fn = i.pathname
188
+ Ditz::debug "issue #{i.name} has been deleted, deleting #{fn}"
189
+ FileUtils.rm fn
190
+ end
191
+
192
+ unless project.added_issues.empty?
193
+ unless Ditz::HookManager.run :after_add, project, config, project.added_issues
194
+ puts "You may have to inform your SCM that the following files have been added:"
195
+ project.added_issues.each { |i| puts " " + i.pathname }
196
+ end
197
+ end
198
+
199
+ unless project.deleted_issues.empty?
200
+ unless Ditz::HookManager.run :after_delete, project, config, project.deleted_issues
201
+ puts "You may have to inform your SCM that the following files have been deleted:"
202
+ project.deleted_issues.each { |i| puts " " + i.pathname }
203
+ end
204
+ end
205
+
206
+ changed_not_added_issues = changed_issues - project.added_issues
207
+ unless changed_not_added_issues.empty?
208
+ Ditz::HookManager.run :after_update, project, config, changed_not_added_issues
209
+ end
210
+
211
+ config.save! $opts[:config_file] if config.changed?
212
+
213
+ # vim: syntax=ruby
@@ -0,0 +1,29 @@
1
+ #compdef ditz
2
+
3
+ ME=ditz
4
+ COMMANDS=--commands
5
+ OPTIONS='<options>'
6
+
7
+ if (($CURRENT == 2)); then
8
+ # We're completing the first word after the tool: the command.
9
+ _wanted command expl "$ME command" \
10
+ compadd -- $( "$ME" "$COMMANDS" )
11
+ else
12
+ # Find the options/files/URL/etc. for the current command by using the tool itself.
13
+ case "${words[$CURRENT]}"; in
14
+ -*)
15
+ _wanted args expl "Arguments for $ME ${words[2]}" \
16
+ compadd -- $( "$ME" "${words[2]}" "$OPTIONS" ; _files )
17
+ ;;
18
+ ht*|ft*)
19
+ _arguments '*:URL:_urls'
20
+ ;;
21
+ /*|./*|\~*|../*)
22
+ _arguments '*:file:_files'
23
+ ;;
24
+ *)
25
+ _wanted args expl "Arguments for $ME ${words[2]}" \
26
+ compadd -- $( "$ME" "${words[2]}" "$OPTIONS" )
27
+ ;;
28
+ esac
29
+ fi
@@ -0,0 +1,22 @@
1
+ # ditz bash completion
2
+ #
3
+ # author: Christian Garbs
4
+ #
5
+ # based on bzr.simple by Martin Pool
6
+
7
+ _ditz()
8
+ {
9
+ cur=${COMP_WORDS[COMP_CWORD]}
10
+ if [ $COMP_CWORD -eq 1 ]; then
11
+ COMPREPLY=( $( compgen -W "$(ditz --commands)" $cur ) )
12
+ elif [ $COMP_CWORD -eq 2 ]; then
13
+ cmd=${COMP_WORDS[1]}
14
+ COMPREPLY=( $( compgen -W "$(ditz "$cmd" '<options>' 2>/dev/null)" $cur ) )
15
+ elif [ $COMP_CWORD -eq 3 ]; then
16
+ cmd=${COMP_WORDS[1]}
17
+ parm1=${COMP_WORDS[2]}
18
+ COMPREPLY=( $( compgen -W "$(ditz "$cmd" "$parm1" '<options>' 2>/dev/null)" $cur ) )
19
+ fi
20
+ }
21
+
22
+ complete -F _ditz -o default ditz
@@ -0,0 +1,22 @@
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>Component <%= component.name %></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><%= project.name %> component: <%= component.name %></h1>
16
+
17
+ <%= render "issue_table", :show_component => false, :show_release => true %>
18
+
19
+ <p class="footer">Generated by <a
20
+ href="http://ditz.rubyforge.org/">ditz</a>.</p>
21
+ </body>
22
+ </html>
data/lib/ditz.rb ADDED
@@ -0,0 +1,56 @@
1
+ module Ditz
2
+
3
+ VERSION = "0.4"
4
+
5
+ def debug s
6
+ puts "# #{s}" if $opts[:verbose]
7
+ end
8
+ module_function :debug
9
+
10
+ def self.has_readline?
11
+ @has_readline
12
+ end
13
+
14
+ def self.has_readline= val
15
+ @has_readline = val
16
+ end
17
+
18
+ begin
19
+ Ditz::has_readline = false
20
+ require 'readline'
21
+ Ditz::has_readline = true
22
+ rescue LoadError
23
+ # do nothing
24
+ end
25
+
26
+ def home_dir
27
+ @home ||=
28
+ ENV["HOME"] || (ENV["HOMEDRIVE"] && ENV["HOMEPATH"] ? ENV["HOMEDRIVE"] + ENV["HOMEPATH"] : nil) || begin
29
+ $stderr.puts "warning: can't determine home directory, using '.'"
30
+ "."
31
+ end
32
+ end
33
+
34
+ ## helper for recursive search
35
+ def find_dir_containing target, start=Pathname.new(".")
36
+ return start if (start + target).exist?
37
+ unless start.parent.realpath == start.realpath
38
+ find_dir_containing target, start.parent
39
+ end
40
+ end
41
+
42
+ ## my brilliant solution to the 'gem datadir' problem
43
+ def find_ditz_file fn
44
+ dir = $:.find { |p| File.exist? File.expand_path(File.join(p, fn)) }
45
+ raise "can't find #{fn} in any load path" unless dir
46
+ File.expand_path File.join(dir, fn)
47
+ end
48
+
49
+ module_function :home_dir, :find_dir_containing, :find_ditz_file
50
+ end
51
+
52
+ require 'model-objects'
53
+ require 'operator'
54
+ require 'views'
55
+ require 'hook'
56
+
data/lib/hook.rb ADDED
@@ -0,0 +1,67 @@
1
+ module Ditz
2
+ class HookManager
3
+ def initialize
4
+ @descs = {}
5
+ @blocks = {}
6
+ end
7
+
8
+ @@instance = nil
9
+ def self.method_missing m, *a, &b
10
+ @@instance ||= self.new
11
+ @@instance.send m, *a, &b
12
+ end
13
+
14
+ def register name, desc
15
+ @descs[name] = desc
16
+ @blocks[name] = []
17
+ end
18
+
19
+ def on *names, &block
20
+ names.each do |name|
21
+ raise "unregistered hook #{name.inspect}" unless @descs[name]
22
+ @blocks[name] << block
23
+ end
24
+ end
25
+
26
+ def run name, *args
27
+ raise "unregistered hook #{name.inspect}" unless @descs[name]
28
+ blocks = hooks_for name
29
+ return false if blocks.empty?
30
+ blocks.each { |block| block[*args] }
31
+ true
32
+ end
33
+
34
+ def print_hooks f=$stdout
35
+ puts <<EOS
36
+ Ditz has #{@descs.size} registered hooks:
37
+
38
+ EOS
39
+
40
+ @descs.map{ |k,v| [k.to_s,v] }.sort.each do |name, desc|
41
+ f.puts <<EOS
42
+ #{name}
43
+ #{"-" * name.length}
44
+ #{desc}
45
+ EOS
46
+ end
47
+ end
48
+
49
+ def enabled? name; !hooks_for(name).empty? end
50
+
51
+ def hooks_for name
52
+ if @blocks[name].nil? || @blocks[name].empty?
53
+ dirs = [Ditz::home_dir, Ditz::find_dir_containing(".ditz")].compact.map do |d|
54
+ File.join d, ".ditz", "hooks"
55
+ end
56
+ Ditz::debug "looking for hooks in #{dirs.join(" and ")}"
57
+ files = dirs.map { |d| Dir[File.join(d, "*.rb")] }.flatten
58
+ files.each do |fn|
59
+ Ditz::debug "loading hook file #{fn}"
60
+ load fn
61
+ end
62
+ end
63
+
64
+ @blocks[name] || []
65
+ end
66
+ end
67
+ end