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/bin/ditz ADDED
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("#{File.dirname($0)}/../lib"))
4
+
5
+ ## requires are split in two for efficiency reasons: ditz should be really
6
+ ## fast when using it for completion.
7
+ $KCODE = "u"
8
+
9
+ require 'ditz/operator'
10
+ op = Ditz::Operator.new
11
+
12
+ ## a secret option for shell completion
13
+ if ARGV.include? '--commands'
14
+ puts op.class.operations.map { |name, _| name }
15
+ exit
16
+ end
17
+
18
+ begin
19
+ require 'rubygems'
20
+ # list version dependant gems here.
21
+ gem 'yaml_waml', '>= 0.3'
22
+ gem 'trollop', '>= 1.9'
23
+ rescue LoadError
24
+ end
25
+
26
+ require 'fileutils'
27
+ require 'pathname'
28
+ require 'trollop'; include Trollop
29
+ require "ditz"
30
+
31
+ CONFIG_FN = ".ditz-config"
32
+ PLUGIN_FN = ".ditz-plugins"
33
+
34
+ config_dir = Ditz::find_dir_containing CONFIG_FN
35
+ plugin_dir = Ditz::find_dir_containing PLUGIN_FN
36
+
37
+ $opts = options do
38
+ version "ditz #{Ditz::VERSION}"
39
+ banner <<EOS
40
+ Usage: ditz [global-opts] [command] [command-opts]
41
+
42
+ See 'ditz help' for a list of commands.
43
+
44
+ Global options are:
45
+ EOS
46
+
47
+ opt :issue_dir, "Issue database dir", :type => :string
48
+ opt :config_file, "Configuration file", :default => File.join(config_dir || ".", CONFIG_FN)
49
+ opt :plugins_file, "Plugins file", :default => File.join(plugin_dir || ".", PLUGIN_FN)
50
+ opt :verbose, "Verbose output", :default => false
51
+ opt :list_hooks, "Print all hooks exit", :short => 'l', :default => false
52
+ opt :version, "Print version and exit", :short => :none
53
+ stop_on_unknown
54
+ end
55
+ $verbose = true if $opts[:verbose]
56
+
57
+ Ditz::HookManager.register :startup, <<EOS
58
+ Executes at startup
59
+
60
+ Variables: project, config
61
+ No return value.
62
+ EOS
63
+
64
+ Ditz::HookManager.register :after_add, <<EOS
65
+ Executes before terminating if new issue files has been created.
66
+ Basically you want to instruct your SCM that these files has
67
+ been added.
68
+
69
+ Variables: project, config, issues
70
+ No return value.
71
+ EOS
72
+
73
+ Ditz::HookManager.register :after_delete, <<EOS
74
+ Executes before terminating if new issue files has been deleted.
75
+ Basically you want to instruct your SCM that these files has
76
+ been deleted.
77
+
78
+ Variables: project, config, issues
79
+ No return value.
80
+ EOS
81
+
82
+ Ditz::HookManager.register :after_update, <<EOS
83
+ Executes before terminating if new issue files has been updated.
84
+ You may want to instruct your SCM about these changes.
85
+ Note that new issues are not considered updated.
86
+
87
+ Variables: project, config, issues
88
+ No return value.
89
+ EOS
90
+
91
+ if $opts[:list_hooks]
92
+ Ditz::HookManager.print_hooks
93
+ exit 0
94
+ end
95
+
96
+ begin
97
+ Ditz::load_plugins $opts[:plugins_file] if File.exist? $opts[:plugins_file]
98
+ rescue LoadError => e
99
+ Ditz::debug "can't load plugins file: #{e.message}"
100
+ end
101
+
102
+ ## prevent ctrl-c and borken pipes from printing a useless backtrace
103
+ def die_gently
104
+ begin
105
+ yield
106
+ rescue Interrupt, Errno::EPIPE
107
+ puts
108
+ exit 1
109
+ end
110
+ end
111
+
112
+ config = begin
113
+ Ditz::debug "loading config from #{$opts[:config_file]}"
114
+ Ditz::Config.from $opts[:config_file]
115
+ rescue SystemCallError => e
116
+ if ARGV.member? "<options>"
117
+ ## special case here. if we're asking for tab completion, and the config
118
+ ## file doesn't exist, don't do the interactive building. just make a
119
+ ## fake empty one and carry on.
120
+ Ditz::Config.new
121
+ else
122
+ puts <<EOS
123
+ I wasn't able to find a configuration file #{$opts[:config_file]}.
124
+ We'll set it up right now.
125
+ EOS
126
+ die_gently { Ditz::Config.create_interactively.save! $opts[:config_file] }
127
+ end
128
+ end
129
+
130
+ ## configure any lowline settings
131
+ Lowline.use_editor_if_possible = config.use_editor_if_possible
132
+
133
+ issue_dir = Pathname.new($opts[:issue_dir] || config.issue_dir)
134
+ cmd = ARGV.shift || "todo"
135
+ unless op.has_operation? cmd
136
+ die "no such command: #{cmd}"
137
+ end
138
+
139
+ ## TODO: refactor so that three 'exit' statements aren't required
140
+ case cmd # some special commands not handled by Ditz::Operator
141
+ when "init"
142
+ die "#{issue_dir} directory already exists" if issue_dir.exist?
143
+ project = nil
144
+ die_gently { project = op.init }
145
+ issue_dir.mkdir
146
+ fn = issue_dir + Ditz::FileStorage::PROJECT_FN
147
+ project.save! fn
148
+ puts "Ok, #{issue_dir} directory created successfully."
149
+ exit
150
+ when "reconfigure" # might not be able to load the project
151
+ die_gently { op.do cmd, nil, config, ARGV }
152
+ exit
153
+ when "help"
154
+ begin
155
+ op.do cmd, nil, config, ARGV
156
+ rescue Ditz::Operator::Error => e
157
+ die "#{e.message}"
158
+ end
159
+ exit
160
+ end
161
+
162
+ $project_root = Ditz::find_dir_containing(issue_dir + Ditz::FileStorage::PROJECT_FN)
163
+ die "No #{issue_dir} directory---use 'ditz init' to initialize" unless $project_root
164
+ $project_root += issue_dir
165
+
166
+ storage = Ditz::FileStorage.new $project_root
167
+ project = begin
168
+ storage.load
169
+ rescue SystemCallError, Ditz::Project::Error => e
170
+ die "#{e.message} (use 'init' to initialize)"
171
+ end
172
+
173
+ Ditz::HookManager.run :startup, project, config
174
+
175
+ Ditz::debug "executing command #{cmd}"
176
+ die_gently do
177
+ begin
178
+ op.do cmd, project, config, ARGV
179
+ ## TODO: make these errors have a common ancestor so that this rescue
180
+ ## statement isn't so stupid
181
+ rescue Ditz::Operator::Error, Ditz::Release::Error, Ditz::Project::Error, Ditz::Issue::Error => e
182
+ ## don't use 'die' here (which is Trollop::die) because this is not a
183
+ ## problem with the command-line arguments.
184
+ $stderr.puts "Error: #{e.message}"
185
+ exit 1
186
+ end
187
+ end
188
+
189
+ changed_issues = project.issues.select { |i| i.changed? }
190
+ changed_not_added_issues = changed_issues - project.added_issues
191
+
192
+ storage.save project
193
+
194
+ ## at this point, for compatibility with older hook stuff, we set the pathname
195
+ ## directly on the issues.
196
+
197
+ project.issues.each { |i| i.pathname = storage.filename_for_issue(i) }
198
+ unless project.added_issues.empty?
199
+ unless Ditz::HookManager.run :after_add, project, config, project.added_issues
200
+ puts "You may have to inform your SCM that the following files have been added:"
201
+ project.added_issues.each { |i| puts " " + storage.filename_for_issue(i) }
202
+ end
203
+ end
204
+
205
+ unless project.deleted_issues.empty?
206
+ unless Ditz::HookManager.run :after_delete, project, config, project.deleted_issues
207
+ puts "You may have to inform your SCM that the following files have been deleted:"
208
+ project.deleted_issues.each { |i| puts " " + storage.filename_for_issue(i) }
209
+ end
210
+ end
211
+
212
+ unless changed_not_added_issues.empty?
213
+ unless Ditz::HookManager.run :after_update, project, config, changed_not_added_issues
214
+ puts "You may have to inform your SCM that the following files have been modified:"
215
+ changed_not_added_issues.each { |i| puts " " + storage.filename_for_issue(i) }
216
+ end
217
+ end
218
+
219
+ ## hack upon a hack
220
+ if project.changed?
221
+ project.pathname = storage.filename_for_project
222
+ unless Ditz::HookManager.run :after_update, project, config, [project]
223
+ puts "You may have to inform your SCM that the following files have been modified:"
224
+ puts " " + storage.filename_for_project
225
+ end
226
+ end
227
+
228
+ config.save! $opts[:config_file] if config.changed?
229
+
230
+ # 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,38 @@
1
+ # ditz bash completion
2
+ #
3
+ # author: Christian Garbs
4
+ #
5
+ # based on bzr.simple by Martin Pool
6
+
7
+ _ditz()
8
+ {
9
+ local cur=${COMP_WORDS[COMP_CWORD]}
10
+
11
+ if [ $COMP_CWORD -eq 1 ]; then
12
+ # no command yet, show all commands
13
+ COMPREPLY=( $( compgen -W "$(ditz --commands)" -- $cur ) )
14
+
15
+ else
16
+ unset COMP_WORDS[COMP_CWORD] # remove last
17
+ unset COMP_WORDS[0] # remove first
18
+
19
+ # add options if applicable...
20
+ local options
21
+ if [ "${cur:0:1}" = '-' ]; then
22
+ # ...but only if at least a dash is given
23
+ case "${COMP_WORDS[1]}" in
24
+ add|add_reference|add_release|assign|close|comment|release|set_component|start|stop|unassign)
25
+ options="--comment --no-comment"
26
+ ;;
27
+ edit)
28
+ options="--comment --no-comment --silent"
29
+ ;;
30
+ esac
31
+ fi
32
+
33
+ # let ditz parse the commandline and print available completions, then append the options form above
34
+ COMPREPLY=( $( compgen -W "$(ditz "${COMP_WORDS[@]}" '<options>' 2>/dev/null) $options" -- $cur ) )
35
+ fi
36
+ }
37
+
38
+ complete -F _ditz -o default ditz
@@ -0,0 +1,53 @@
1
+ module Ditz
2
+
3
+ ## stores ditz database on disk
4
+ class FileStorage
5
+ PROJECT_FN = "project.yaml"
6
+ ISSUE_FN_GLOB = "issue-*.yaml"
7
+
8
+ def ISSUE_TO_FN i; "issue-#{i.id}.yaml" end
9
+
10
+ def initialize base_dir
11
+ @base_dir = base_dir
12
+ @project_fn = File.join @base_dir, PROJECT_FN
13
+ end
14
+
15
+ def load
16
+ Ditz::debug "loading project from #{@project_fn}"
17
+ project = Project.from @project_fn
18
+
19
+ fn = File.join @base_dir, ISSUE_FN_GLOB
20
+ Ditz::debug "loading issues from #{fn}"
21
+ project.issues = Dir[fn].map { |fn| Issue.from fn }
22
+ Ditz::debug "found #{project.issues.size} issues"
23
+
24
+ project.issues.each { |i| i.project = project }
25
+ project
26
+ end
27
+
28
+ def save project
29
+ dirty = project.each_modelobject { |o| break true if o.changed? }
30
+ if dirty
31
+ Ditz::debug "project is dirty, saving #{@project_fn}"
32
+ project.save! @project_fn
33
+ end
34
+
35
+ changed_issues = project.issues.select { |i| i.changed? }
36
+ changed_issues.each do |i|
37
+ fn = filename_for_issue i
38
+ Ditz::debug "issue #{i.name} is dirty, saving #{fn}"
39
+ i.save! fn
40
+ end
41
+
42
+ project.deleted_issues.each do |i|
43
+ fn = filename_for_issue i
44
+ Ditz::debug "issue #{i.name} has been deleted, deleting #{fn}"
45
+ FileUtils.rm fn
46
+ end
47
+ end
48
+
49
+ def filename_for_issue i; File.join @base_dir, ISSUE_TO_FN(i) end
50
+ def filename_for_project; @project_fn end
51
+ end
52
+
53
+ end
data/lib/ditz/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
+ require File.expand_path(fn)
61
+ end
62
+ end
63
+
64
+ @blocks[name] || []
65
+ end
66
+ end
67
+ end
data/lib/ditz/html.rb ADDED
@@ -0,0 +1,107 @@
1
+ require 'erb'
2
+
3
+ module Ditz
4
+
5
+ ## pass through any variables needed for template generation, and add a bunch
6
+ ## of HTML formatting utility methods.
7
+ class ErbHtml
8
+ def initialize template_dir, links, binding={}
9
+ @template_dir = template_dir
10
+ @links = links
11
+ @binding = binding
12
+ end
13
+
14
+ ## return an ErbHtml object that has the current binding plus extra_binding merged in
15
+ def clone_for_binding extra_binding={}
16
+ extra_binding.empty? ? self : ErbHtml.new(@template_dir, @links, @binding.merge(extra_binding))
17
+ end
18
+
19
+ def render_template template_name, extra_binding={}
20
+ if extra_binding.empty?
21
+ @@erbs ||= {}
22
+ @@erbs[template_name] ||= ERB.new IO.read(File.join(@template_dir, "#{template_name}.rhtml"))
23
+ @@erbs[template_name].result binding
24
+ else
25
+ clone_for_binding(extra_binding).render_template template_name
26
+ end
27
+ end
28
+
29
+ def render_string s, extra_binding={}
30
+ if extra_binding.empty?
31
+ ERB.new(s).result binding
32
+ else
33
+ clone_for_binding(extra_binding).render_string s
34
+ end
35
+ end
36
+
37
+ ###
38
+ ### the following methods are meant to be called from the ERB itself
39
+ ###
40
+
41
+ def h o; o.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") end
42
+ def t o; o.strftime "%Y-%m-%d %H:%M %Z" end
43
+ def p o; "<p>" + h(o.to_s).gsub("\n\n", "</p><p>") + "</p>" end
44
+ def obscured_email e; h e.gsub(/@.*?(>|$)/, "@...\\1") end
45
+ def link_to o, name
46
+ dest = @links[o]
47
+ dest = o if dest.nil? && o.is_a?(String)
48
+ raise ArgumentError, "no link for #{o.inspect}" unless dest
49
+ "<a href=\"#{dest}\">#{name}</a>"
50
+ end
51
+
52
+ def issue_status_img_for i, opts={}
53
+ fn, title = if i.closed?
54
+ case i.disposition
55
+ when :fixed; ["green-check.png", "fixed"]
56
+ when :wontfix; ["red-check.png", "won't fix"]
57
+ when :reorg; ["blue-check.png", "reorganized"]
58
+ end
59
+ elsif i.in_progress?
60
+ ["green-bar.png", "in progress"]
61
+ elsif i.paused?
62
+ ["yellow-bar.png", "paused"]
63
+ end
64
+
65
+ return "" unless fn
66
+
67
+ args = {:src => fn, :alt => title, :title => title}
68
+ args[:class] = opts[:class] if opts[:class]
69
+
70
+ "<img " + args.map { |k, v| "#{k}=#{v.inspect}" }.join(" ") + "/>"
71
+ end
72
+
73
+ def issue_link_for i, opts={}
74
+ link = if opts[:inline]
75
+ "<span class=\"inline-issue-link\">" + link_to(i, "issue <span class=\"id\">#{i.id[0,8]}</span>: #{i.title}") + "</span>"
76
+ else
77
+ link_to i, i.title
78
+ end
79
+ link = link + " " + issue_status_img_for(i, :class => "inline-status-image") if opts[:status_image]
80
+ link
81
+ end
82
+
83
+ def link_issue_names project, s, opts={}
84
+ project.issues.inject(s) do |s, i|
85
+ s.gsub(/\b#{i.name}\b/, issue_link_for(i, {:inline => true, :status_image => true}.merge(opts)))
86
+ end
87
+ end
88
+
89
+ def progress_meter p, size=50
90
+ done = (p * size).to_i
91
+ undone = [size - done, 0].max
92
+ "<span class='progress-meter'><span class='progress-meter-done'>" +
93
+ ("&nbsp;" * done) +
94
+ "</span><span class='progress-meter-undone'>" +
95
+ ("&nbsp;" * undone) +
96
+ "</span></span>"
97
+ end
98
+
99
+ ## render a nested ERB
100
+ alias :render :render_template
101
+
102
+ def method_missing meth, *a
103
+ @binding.member?(meth) ? @binding[meth] : super
104
+ end
105
+ end
106
+
107
+ end