Commit f9a14f1a8b304ceac6e7045c433adf44d391cd0b

Merge branch 'master' into adoi+fh

Conflicts:

bin/ditz
lib/operator.rb

Commit diff

.gitignore

 
22*.swp
33bugs.yaml~
44.ditz-config
5.ditz-plugins
toggle raw diff

Changelog

 
1== 0.3 / 2008-06-04
2* readline support for all text entry
3* hook system. Use ditz -l to see possible hooks.
4* new commands: archive, shortlog, set-component
5* improved commands: log, assign, add-release
6* new issue type: 'tasks'
7* 'ditz' by itself shows the todo list
8* zsh tab completion for subcommands
9* local config can now specify bugs directory location
10* issue name interpolation now on all issue fields
11* bugfix: various HTML generation bugs
12* bugfix: ditz now works from project subdirectories
13* bugfix: removed UNIX-specific environment variable assumptions
114== 0.2 / 2008-04-11
215* bugfix: store each issue in a separate file to avoid false conflicts
316* added per-command help
toggle raw diff

Manifest.txt

 
55bin/ditz
66bin/ditz-convert-from-monolith
77lib/ditz.rb
8lib/hook.rb
89lib/html.rb
910lib/index.rhtml
1011lib/issue.rhtml
1515lib/model-objects.rb
1616lib/model.rb
1717lib/operator.rb
18lib/plugins/git-features.rb
1819lib/release.rhtml
1920lib/style.css
2021lib/util.rb
21lib/hook.rb
22lib/view.rb
23lib/views.rb
toggle raw diff

Rakefile

 
1717 p.url = "http://ditz.rubyforge.org"
1818 p.changes = p.paragraphs_of('Changelog', 0..0).join("\n\n")
1919 p.email = "wmorgan-ditz@masanjin.net"
20 p.extra_deps = [['trollop', '>= 1.7']]
20 p.extra_deps = [['trollop', '>= 1.8.2']]
2121end
2222
2323WWW_FILES = FileList["www/*"] + %w(README.txt)
toggle raw diff

ReleaseNotes

 
10.3
2---
3Ditz now works from project subdirectories, and you can have a .ditz-config in
4the project root for project-specific configuration. (This is not merged with
5the global config, so this file overrides everything in ~/.ditz-config.)
6
7You can specify an :issue_dir key in this file, which can be a relative path to
8the directory containing project.yaml. So if you want to rename that directory,
9or keep it somewhere else, now you can.
10
11There's also a new hook system for plugging in your own code. Run ditz -l to
12see a list of available hooks.
13
1140.2
215---
316
toggle raw diff

bin/ditz

 
11#!/usr/bin/env ruby
22
3# requires are splitted in two for efficiency reasons
4# ditz should be really fast when using it for
5# completion.
3## requires are split in two for efficiency reasons: ditz should be really
4## fast when using it for completion.
65require 'operator'
7
86op = Ditz::Operator.new
97
10# a secret option for shell completion
8## a secret option for shell completion
119if ARGV.include? '--commands'
1210 puts op.class.operations.map { |name, _| name }
1311 exit 0
1313
1414require 'rubygems'
1515require 'fileutils'
16require 'pathname'
1617require 'trollop'; include Trollop
1718require "ditz"
1819
1920PROJECT_FN = "project.yaml"
2021CONFIG_FN = ".ditz-config"
22PLUGIN_FN = ".ditz-plugins"
23
24## helper for recursive search
25def find_dir_containing target, start=Pathname.new(".")
26 return start if (start + target).exist?
27 unless start.parent.realpath == start.realpath
28 find_dir_containing target, start.parent
29 end
30end
31
32## my brilliant solution to the 'gem datadir' problem
33def find_ditz_file fn
34 dir = $:.find { |p| File.exist? File.expand_path(File.join(p, fn)) }
35 raise "can't find #{fn} in any load path" unless dir
36 File.expand_path File.join(dir, fn)
37end
38
39config_dir = find_dir_containing CONFIG_FN
40plugin_dir = find_dir_containing PLUGIN_FN
2141
2242$opts = options do
2343 version "ditz #{Ditz::VERSION}"
2444 opt :issue_dir, "Issue database dir", :default => "bugs"
25 opt :config_file, "Configuration file", :default => File.join(ENV["HOME"], CONFIG_FN)
45 opt :config_file, "Configuration file", :default => File.join(config_dir || ".", CONFIG_FN)
46 opt :plugins_file, "Plugins file", :default => File.join(plugin_dir || ".", PLUGIN_FN)
2647 opt :verbose, "Verbose output", :default => false
2748 opt :no_comment, "Skip asking for a comment", :default => false
2849 opt :list_hooks, "List all hooks and descriptions, and quit.", :short => 'l', :default => false
8787 Ditz::HookManager.print_hooks
8888 exit 0
8989end
90cmd = ARGV.shift or die "expecting a ditz command"
91dir = Pathname.new($opts[:issue_dir])
92Ditz::Issue::ISSUE_DIR = dir
9390
94case cmd # some special cases not handled by Ditz::Operator
91plugins = begin
92 Ditz::debug "loading plugins from #{$opts[:plugins_file]}"
93 YAML::load_file$opts[:plugins_file]
94rescue SystemCallError => e
95 Ditz::debug "can't load plugins file: #{e.message}"
96 []
97end
98
99plugins.each do |p|
100 fn = find_ditz_file "plugins/#{p}.rb"
101 Ditz::debug "loading plugin #{p.inspect} from #{fn}"
102 load fn
103end
104
105config = begin
106 Ditz::debug "loading config from #{$opts[:config_file]}"
107 Ditz::Config.from $opts[:config_file]
108rescue SystemCallError => e
109 puts <<EOS
110I wasn't able to find a configuration file #{$opts[:config_file]}.
111We'll set it up right now.
112EOS
113 Ditz::Config.create_interactively.save! $opts[:config_file]
114end
115
116cmd = ARGV.shift || "todo"
117issue_dir = Pathname.new(config.issue_dir || $opts[:issue_dir])
118
119case cmd # some special commands not handled by Ditz::Operator
95120when "init"
96 die "#{dir} directory already exists" if dir.exist?
97 dir.mkdir
98 fn = dir + PROJECT_FN
121 die "#{issue_dir} directory already exists" if issue_dir.exist?
122 issue_dir.mkdir
123 fn = issue_dir + PROJECT_FN
99124 project = op.init
100125 project.save! fn
101 puts "Ok, #{dir} directory created successfully."
126 puts "Ok, #{issue_dir} directory created successfully."
102127 exit
103128when "help"
104129 op.do "help", nil, nil, ARGV
105130 exit
106131end
107132
108project_root = Ditz::find_project_root Pathname.pwd, Pathname.new('.'), dir
109die "No #{dir} directory---use 'ditz init' to initialize" unless project_root != nil and project_root.exist?
110Dir.chdir project_root
133project_root = find_dir_containing(issue_dir + PROJECT_FN)
134die "No #{issue_dir} directory---use 'ditz init' to initialize" unless project_root
135project_root += issue_dir
111136
112137project = begin
113 fn = dir + PROJECT_FN
138 fn = project_root + PROJECT_FN
114139 Ditz::debug "loading project from #{fn}"
115140 project = Ditz::Project.from fn
116141
117 fn = dir + "issue-*.yaml"
142 fn = project_root + "issue-*.yaml"
118143 Ditz::debug "loading issues from #{fn}"
119144 project.issues = Dir[fn].map { |fn| Ditz::Issue.from fn }
120145 Ditz::debug "found #{project.issues.size} issues"
151151project.each_modelobject { |o| o.after_deserialize project }
152152project.issues.each { |o| o.after_deserialize project }
153153project.validate!
154project.issues.each { |p| p.project = project}
154155project.assign_issue_names!
155156
156config = begin
157 if File.exists? CONFIG_FN
158 Ditz::debug "loading local config from #{CONFIG_FN}"
159 Ditz::Config.from CONFIG_FN
160 else
161 Ditz::debug "loading global config from #{$opts[:config_file]}"
162 Ditz::Config.from $opts[:config_file]
163 end
164rescue SystemCallError, Ditz::ModelObject::ModelError => e
165 puts <<EOS
166I wasn't able to find a configuration file #{$opts[:config_file]}.
167We'll set it up right now.
168EOS
169 Ditz::Config.create_interactively
170end
171
172157unless op.has_operation? cmd
173158 die "no such command: #{cmd}"
174159end
169169 op.do cmd, project, config, args
170170rescue Ditz::Operator::Error => e
171171 die e.message
172rescue Interrupt
172rescue Errno::EPIPE, Interrupt
173173 exit 1
174174end
175175
176176## save project.yaml
177177dirty = project.each_modelobject { |o| break true if o.changed? } || false
178178if dirty
179 fn = dir + PROJECT_FN
179 fn = project_root + PROJECT_FN
180180 Ditz::debug "project is dirty, saving #{fn}"
181 project.each_modelobject { |o| o.before_serialize project }
182181 project.save! fn
183182end
184183
185changed_issues = project.issues.select { |i| i.changed? }
186
187184## project issues are not model fields proper, so they must be
188185## saved independently.
186changed_issues = project.issues.select { |i| i.changed? }
189187changed_issues.each do |i|
190 i.before_serialize project
191 fn = i.pathname
192 Ditz::debug "issue #{i.name} is dirty, saving #{fn}"
193 i.save! fn
188 i.pathname ||= (project_root + "issue-#{i.id}.yaml")
189 i.project ||= project # hack: not set on new issues
190 Ditz::debug "issue #{i.name} is dirty, saving #{i.pathname}"
191 i.save! i.pathname
194192end
195193
196194project.deleted_issues.each do |i|
toggle raw diff

bin/ditz-convert-from-monolith

 
0#!/usr/bin/env ruby
1
2require 'rubygems'
3require 'fileutils'
4require "ditz"
5
6PROJECT_FN = "project.yaml"
7CONFIG_FN = ".ditz-config"
8def ISSUE_TO_FN i; "issue-#{i.id}.yaml" end
9
10dir = "bugs"
11project = Ditz::Project.from "bugs.yaml"
12puts "making #{dir}"
13FileUtils.mkdir dir
14project.changed!
15project.issues.each { |i| i.changed! }
16
17project.validate!
18project.assign_issue_names!
19project.each_modelobject { |o| o.after_deserialize project }
20
21## save project.yaml
22dirty = project.each_modelobject { |o| break true if o.changed? } || false
23if dirty
24 fn = File.join dir, PROJECT_FN
25 puts "writing #{fn}"
26 project.each_modelobject { |o| o.before_serialize project }
27 project.save! fn
28end
29
30## project issues are not model fields proper, so they must be
31## saved independently.
32project.issues.each do |i|
33 if i.changed?
34 i.before_serialize project
35 fn = File.join dir, ISSUE_TO_FN(i)
36 puts "writing #{fn}"
37 i.save! fn
38 end
39end
40
41puts "You can delete bugs.yaml now."
toggle raw diff

contrib/completion/ditz.bash

 
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 COMPREPLY=( $( compgen -W "$(ditz todo-full 2>/dev/null | grep '^. ' | cut -c 3- | cut -d : -f 1)" $cur ) )
14 fi
15}
16
17complete -F _ditz -o default ditz
toggle raw diff

lib/ditz.rb

 
1require 'pathname'
21module Ditz
32
4VERSION = "0.2"
5@has_readline=false
3VERSION = "0.3"
64
75def debug s
86 puts "# #{s}" if $opts[:verbose]
1111 @has_readline
1212end
1313
14def find_project_root(pwd, project_root, dir)
15 np = pwd.join project_root, dir, "project.yaml"
16 if np.exist?
17 return project_root
18 else
19 if pwd + project_root != pwd + project_root.parent
20 find_project_root pwd, project_root.parent, dir
21 else
22 return nil
23 end
24 end
14def self.has_readline= val
15 @has_readline = val
2516end
26
27def self.has_readline=(val)
28 @has_readline=val
29end
30
31module_function :find_project_root
3217end
3318
3419begin
20 Ditz::has_readline = false
3521 require 'readline'
36 Ditz::has_readline=true
22 Ditz::has_readline = true
3723rescue LoadError
3824 # do nothing
3925end
4026
4127require 'model-objects'
4228require 'operator'
29require 'views'
4330require 'hook'
toggle raw diff

lib/hook.rb

 
11module Ditz
2 module HookManager
3 module_function
2 class HookManager
3 def initialize
4 @descs = {}
5 @blocks = {}
6 end
47
5 @@descs = {}
6 @@blocks = {}
8 @@instance = nil
9 def self.method_missing m, *a, &b
10 @@instance ||= self.new
11 @@instance.send m, *a, &b
12 end
713
814 def register name, desc
9 raise "Ditz::HookManager.register needs a symbol not #{name.inspect}" unless name.is_a? Symbol
10 @@descs[name] = desc
11 @@blocks[name] = []
15 @descs[name] = desc
16 @blocks[name] = []
1217 end
1318
1419 def on *names, &block
15 for name in names do
16 raise "unregistered hook #{name.inspect}" unless @@descs[name]
17 @@blocks[name] << block
20 names.each do |name|
21 raise "unregistered hook #{name.inspect}" unless @descs[name]
22 @blocks[name] << block
1823 end
1924 end
2025
2126 def run name, *args
22 raise "unregistered hook #{name.inspect}" unless @@descs[name]
27 raise "unregistered hook #{name.inspect}" unless @descs[name]
2328 blocks = hooks_for name
2429 return false if blocks.empty?
25 for block in blocks do
26 block[*args]
27 end
30 blocks.each { |block| block[*args] }
2831 true
2932 end
3033
3134 def print_hooks f=$stdout
3235puts <<EOS
33Ditz have #{@@descs.size} registered hooks:
36Ditz has #{@descs.size} registered hooks:
3437
3538EOS
3639
37 @@descs.map{ |k,v| [k.to_s,v] }.sort.each do |(name, desc)|
40 @descs.map{ |k,v| [k.to_s,v] }.sort.each do |name, desc|
3841 f.puts <<EOS
3942#{name}
4043#{"-" * name.length}
4949 def enabled? name; !hooks_for(name).empty? end
5050
5151 def hooks_for name
52 if @@blocks[name].nil? || @@blocks[name].empty?
52 if @blocks[name].nil? || @blocks[name].empty?
5353 fns = File.join(ENV['HOME'], '.ditz', 'hooks', '*.rb')
5454 Dir[fns].each { |fn| load fn }
5555 end
5656
57 @@blocks[name] || []
57 @blocks[name] || []
5858 end
5959 end
60
6160end
toggle raw diff

lib/html.rb

 
55## pass through any variables needed for template generation, and add a bunch
66## of HTML formatting utility methods.
77class ErbHtml
8 def initialize template_dir, template_name, links, mapping={}
9 @template_name = template_name
8 def initialize template_dir, links, binding={}
109 @template_dir = template_dir
1110 @links = links
12 @mapping = mapping
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
1328
14 @@erbs ||= {}
15 @@erbs[template_name] ||= ERB.new(IO.readlines(File.join(template_dir, "#{template_name}.rhtml")).join)
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
1635 end
1736
37 ###
38 ### the following methods are meant to be called from the ERB itself
39 ###
40
1841 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
1943 def p o; "<p>" + h(o.to_s).gsub("\n\n", "</p><p>") + "</p>" end
2044 def obscured_email e; h e.gsub(/@.*?(>|$)/, "@...\\1") end
2145 def link_to o, name
4949 "<a href=\"#{dest}\">#{name}</a>"
5050 end
5151
52 def render template_name, morevars={}
53 ErbHtml.new(@template_dir, template_name, @links, @mapping.merge(morevars)).to_s
52 def link_issue_names project, s
53 project.issues.inject(s) do |s, i|
54 s.gsub(/\b#{i.name}\b/, link_to(i, i.closed? ? "#{i.title} (#{i.disposition})" : i.title))
55 end
5456 end
5557
56 def method_missing meth, *a
57 @mapping.member?(meth) ? @mapping[meth] : super
58 end
58 ## render a nested ERB
59 alias :render :render_template
5960
60 def to_s
61 @@erbs[@template_name].result binding
61 def method_missing meth, *a
62 @binding.member?(meth) ? @binding[meth] : super
6263 end
6364end
6465
toggle raw diff

lib/index.rhtml

 
2626 no issues
2727 <% else %>
2828 <%= sprintf "%.0f%%", pct_done %> complete;
29 <%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
29 <% if open_issues.empty? %>
30 ready for release!
31 <% else %>
32 <%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
33 <% end %>
3034 <% end %>
3135 </li>
3236 <% end %>
5454 open_issues = issues.select { |i| i.open? }
5555%>
5656<p>
57 <%= link_to "unassigned", "unassigned issue".pluralize(issues.size).capitalize %>; <%= open_issues.size.to_pretty_s %> open.
57 <% if issues.empty? %>
58 No unassigned issues.
59 <% else %>
60 <%= link_to "unassigned", "unassigned issue".pluralize(issues.size).capitalize %>; <%= open_issues.size.to_pretty_s %> of them open.
61 <% end %>
5862</p>
5963
6064<% if components.size > 1 %>
6969 open_issues = project.group_issues(project.issues_for_component(c).select { |i| i.open? })
7070 %>
7171 <li>
72 <%= link_to c, c.name %>:
7372 <% if open_issues.empty? %>
73 <span class="dimmed">
74 <%= link_to c, c.name %>:
7475 no open issues.
76 </span>
7577 <% else %>
78 <%= link_to c, c.name %>:
7679 <%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
7780 <% end %>
7881 </li>
8383 </ul>
8484<% end %>
8585
86<h2>Recently modified issues</h2>
87
88<table>
89<% project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
90 flatten_one_level.
91 sort_by { |e| e.first.first }.
92 uniq_by { |stuff, i| i }.
93 sort_by { |e| e.first.first }.
94 reverse[0 ... 10].
95 each do |(date, author, what, comment), i| %>
96 <tr>
97 <td><%= date.pretty_date %></td>
98 <td class="issuestatus_<%= i.status %>">
99 <% if i.closed? %>
100 <%= i.disposition_string %>
101 <% else %>
102 <%= i.status_string %>
103 <% end %>
104 </td>
105 <td class="issuename">
106 <%= link_to i, i.title %>
107 <%= i.bug? ? '(bug)' : '' %>
108 </td>
109 <td><%= what %></td>
110 </tr>
111<% end %>
112</table>
113
86114<p class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.
87115
88116</body>
toggle raw diff

lib/issue.rhtml

 
99
1010<%= link_to "index", "&laquo; #{project.name} project page" %>
1111
12<h1><%= issue.title %></h1>
12<h1><%= link_issue_names project, issue.title %></h1>
1313
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) %>
1915
2016<table>
2117 <tr>
7373 <%= issue.status_string %><% if issue.closed? %>: <%= issue.disposition_string %><% end %>
7474 </td>
7575 </tr>
76
77 <%= extra_summary_html %>
78
7679</table>
7780
81<%= extra_details_html %>
82
7883<h2>Issue log</h2>
7984
8085<table>
8989<% else %>
9090 <tr class="logentryodd">
9191<% end %>
92 <td class="logtime"><%=h time %></td>
92 <td class="logtime"><%=t time %></td>
9393 <td class="logwho"><%=obscured_email who %></td>
9494 <td class="logwhat"><%=h what %></td>
9595 </tr>
9696 <tr><td colspan="3" class="logcomment">
9797 <% if comment.empty? %>
9898 <% else %>
99 <%=p comment %>
99 <%= link_issue_names project, p(comment) %>
100100 <% end %>
101101 </td></tr>
102102<% end %>
toggle raw diff