ditz 0.2 → 0.3

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/Changelog CHANGED
@@ -1,3 +1,16 @@
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
1
14
  == 0.2 / 2008-04-11
2
15
  * bugfix: store each issue in a separate file to avoid false conflicts
3
16
  * added per-command help
data/README.txt CHANGED
@@ -8,14 +8,14 @@ http://ditz.rubyforge.org
8
8
 
9
9
  Ditz is a simple, light-weight distributed issue tracker designed to work with
10
10
  distributed version control systems like darcs and git. Ditz maintains an issue
11
- database file on disk, written in a line-based and human-editable format. This
12
- file is kept under version control, alongside project code. Changes in issue
13
- state is handled by version control like code change: included as part of a
14
- commit, merged with changes from other developers, conflict-resolved in the
15
- standard manner, etc.
11
+ database directory on disk, with files written in a line-based and human-
12
+ editable format. This directory is kept under version control alongside
13
+ project code. Changes in issue state is handled by version control like code
14
+ change: included as part of a commit, merged with changes from other
15
+ developers, conflict-resolved in the standard manner, etc.
16
16
 
17
17
  Ditz provides a simple, console-based interface for creating and updating the
18
- issue database file, and some rudimentary HTML generation capabilities for
18
+ issue database files, and some rudimentary HTML generation capabilities for
19
19
  producing world-readable status pages. It offers no central public method of
20
20
  bug submission.
21
21
 
data/Rakefile CHANGED
@@ -33,11 +33,11 @@ SCREENSHOTS.each do |fn|
33
33
  end
34
34
 
35
35
  task :upload_webpage => WWW_FILES do |t|
36
- sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
36
+ sh "rsync -essh -cavz #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
37
37
  end
38
38
 
39
39
  task :upload_webpage_images => (SCREENSHOTS + SCREENSHOTS_SMALL) do |t|
40
- sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
40
+ sh "rsync -essh -cavs #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
41
41
  end
42
42
 
43
43
  task :upload_report do |t|
@@ -1,3 +1,16 @@
1
+ 0.3
2
+ ---
3
+ Ditz now works from project subdirectories, and you can have a .ditz-config in
4
+ the project root for project-specific configuration. (This is not merged with
5
+ the global config, so this file overrides everything in ~/.ditz-config.)
6
+
7
+ You can specify an :issue_dir key in this file, which can be a relative path to
8
+ the directory containing project.yaml. So if you want to rename that directory,
9
+ or keep it somewhere else, now you can.
10
+
11
+ There's also a new hook system for plugging in your own code. Run ditz -l to
12
+ see a list of available hooks.
13
+
1
14
  0.2
2
15
  ---
3
16
 
data/bin/ditz CHANGED
@@ -1,47 +1,126 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
+ ## requires are split in two for efficiency reasons: ditz should be really
4
+ ## fast when using it for completion.
5
+ require 'operator'
6
+ op = Ditz::Operator.new
7
+
8
+ ## a secret option for shell completion
9
+ if ARGV.include? '--commands'
10
+ puts op.class.operations.map { |name, _| name }
11
+ exit 0
12
+ end
13
+
3
14
  require 'rubygems'
4
15
  require 'fileutils'
16
+ require 'pathname'
5
17
  require 'trollop'; include Trollop
6
18
  require "ditz"
7
19
 
8
20
  PROJECT_FN = "project.yaml"
9
21
  CONFIG_FN = ".ditz-config"
10
- def ISSUE_TO_FN i; "issue-#{i.id}.yaml" end
22
+
23
+ ## helper for recursive search
24
+ def find_dir_containing target, start=Pathname.new(".")
25
+ return start if (start + target).exist?
26
+ unless start.parent.realpath == start.realpath
27
+ find_dir_containing target, start.parent
28
+ end
29
+ end
11
30
 
12
31
  $opts = options do
13
32
  version "ditz #{Ditz::VERSION}"
14
33
  opt :issue_dir, "Issue database dir", :default => "bugs"
15
34
  opt :config_file, "Configuration file", :default => File.join(ENV["HOME"], CONFIG_FN)
16
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
17
38
  end
18
39
 
19
- cmd = ARGV.shift or die "expecting a ditz command"
20
- op = Ditz::Operator.new
21
- dir = $opts[:issue_dir]
40
+ Ditz::HookManager.register :startup, <<EOS
41
+ Executes at startup
42
+
43
+ Variables: project, config
44
+ No return value.
45
+ EOS
46
+
47
+ Ditz::HookManager.register :after_add, <<EOS
48
+ Executes before terminating if new issue files has been created.
49
+ Basically you want to instruct your SCM that these files has
50
+ been added.
51
+
52
+ Variables: project, config, issues
53
+ No return value.
54
+ EOS
55
+
56
+ Ditz::HookManager.register :after_delete, <<EOS
57
+ Executes before terminating if new issue files has been deleted.
58
+ Basically you want to instruct your SCM that these files has
59
+ been deleted.
60
+
61
+ Variables: project, config, issues
62
+ No return value.
63
+ EOS
64
+
65
+ Ditz::HookManager.register :after_update, <<EOS
66
+ Executes before terminating if new issue files has been updated.
67
+ You may want to instruct your SCM about these changes.
68
+ Note that new issues are not considered updated.
69
+
70
+ Variables: project, config, issues
71
+ No return value.
72
+ EOS
73
+
74
+ if $opts[:list_hooks]
75
+ Ditz::HookManager.print_hooks
76
+ exit 0
77
+ end
78
+
79
+ local_config_dir = find_dir_containing CONFIG_FN
80
+ config = begin
81
+ if local_config_dir
82
+ fn = local_config_dir + CONFIG_FN
83
+ Ditz::debug "loading local config from #{fn}"
84
+ Ditz::Config.from fn
85
+ else
86
+ Ditz::debug "loading global config from #{$opts[:config_file]}"
87
+ Ditz::Config.from $opts[:config_file]
88
+ end
89
+ rescue SystemCallError, Ditz::ModelObject::ModelError => e
90
+ puts <<EOS
91
+ I wasn't able to find a configuration file #{$opts[:config_file]}.
92
+ We'll set it up right now.
93
+ EOS
94
+ Ditz::Config.create_interactively.save! $opts[:config_file]
95
+ end
22
96
 
23
- case cmd # some special cases not handled by Ditz::Operator
97
+ cmd = ARGV.shift || "todo"
98
+ issue_dir = Pathname.new(config.issue_dir || $opts[:issue_dir])
99
+
100
+ case cmd # some special commands not handled by Ditz::Operator
24
101
  when "init"
25
- die "#{dir} directory already exists" if File.exists? dir
26
- FileUtils.mkdir dir
27
- fn = File.join dir, PROJECT_FN
102
+ die "#{issue_dir} directory already exists" if issue_dir.exist?
103
+ issue_dir.mkdir
104
+ fn = issue_dir + PROJECT_FN
28
105
  project = op.init
29
106
  project.save! fn
30
- puts "Ok, #{dir} directory created successfully."
107
+ puts "Ok, #{issue_dir} directory created successfully."
31
108
  exit
32
109
  when "help"
33
110
  op.do "help", nil, nil, ARGV
34
111
  exit
35
112
  end
36
113
 
37
- die "No #{dir} directory---use 'ditz init' to initialize" unless File.exists? dir
114
+ project_root = find_dir_containing(issue_dir + PROJECT_FN)
115
+ die "No #{issue_dir} directory---use 'ditz init' to initialize" unless project_root
116
+ project_root += issue_dir
38
117
 
39
118
  project = begin
40
- fn = File.join dir, PROJECT_FN
119
+ fn = project_root + PROJECT_FN
41
120
  Ditz::debug "loading project from #{fn}"
42
121
  project = Ditz::Project.from fn
43
122
 
44
- fn = File.join dir, "issue-*.yaml"
123
+ fn = project_root + "issue-*.yaml"
45
124
  Ditz::debug "loading issues from #{fn}"
46
125
  project.issues = Dir[fn].map { |fn| Ditz::Issue.from fn }
47
126
  Ditz::debug "found #{project.issues.size} issues"
@@ -51,29 +130,15 @@ rescue SystemCallError, Ditz::Project::Error => e
51
130
  end
52
131
 
53
132
  project.validate!
133
+ project.issues.each { |p| p.project = project}
54
134
  project.assign_issue_names!
55
- project.each_modelobject { |o| o.after_deserialize project }
56
-
57
- config = begin
58
- if File.exists? CONFIG_FN
59
- Ditz::debug "loading local config from #{CONFIG_FN}"
60
- Ditz::Config.from CONFIG_FN
61
- else
62
- Ditz::debug "loading global config from #{$opts[:config_file]}"
63
- Ditz::Config.from $opts[:config_file]
64
- end
65
- rescue SystemCallError, Ditz::ModelObject::ModelError => e
66
- puts <<EOS
67
- I wasn't able to find a configuration file #{$opts[:config_file]}.
68
- We'll set it up right now.
69
- EOS
70
- Ditz::Config.create_interactively
71
- end
72
135
 
73
136
  unless op.has_operation? cmd
74
137
  die "no such command: #{cmd}"
75
138
  end
76
139
 
140
+ Ditz::HookManager.run :startup, project, config
141
+
77
142
  ## talk about the law of unintended consequences. 'gets' requires this.
78
143
  args = []
79
144
  args << ARGV.shift until ARGV.empty?
@@ -83,44 +148,51 @@ begin
83
148
  op.do cmd, project, config, args
84
149
  rescue Ditz::Operator::Error => e
85
150
  die e.message
86
- rescue Interrupt
151
+ rescue Errno::EPIPE, Interrupt
87
152
  exit 1
88
153
  end
89
154
 
90
155
  ## save project.yaml
91
156
  dirty = project.each_modelobject { |o| break true if o.changed? } || false
92
157
  if dirty
93
- fn = File.join dir, PROJECT_FN
158
+ fn = project_root + PROJECT_FN
94
159
  Ditz::debug "project is dirty, saving #{fn}"
95
- project.each_modelobject { |o| o.before_serialize project }
96
160
  project.save! fn
97
161
  end
98
162
 
99
163
  ## project issues are not model fields proper, so they must be
100
164
  ## saved independently.
101
- project.issues.each do |i|
102
- if i.changed?
103
- i.before_serialize project
104
- fn = File.join dir, ISSUE_TO_FN(i)
105
- Ditz::debug "issue #{i.name} is dirty, saving #{fn}"
106
- i.save! fn
107
- end
165
+ changed_issues = project.issues.select { |i| i.changed? }
166
+ changed_issues.each do |i|
167
+ i.pathname ||= (project_root + "issue-#{i.id}.yaml")
168
+ i.project ||= project # hack: not set on new issues
169
+ Ditz::debug "issue #{i.name} is dirty, saving #{i.pathname}"
170
+ i.save! i.pathname
108
171
  end
109
172
 
110
173
  project.deleted_issues.each do |i|
111
- fn = File.join dir, ISSUE_TO_FN(i)
174
+ fn = i.pathname
112
175
  Ditz::debug "issue #{i.name} has been deleted, deleting #{fn}"
113
176
  FileUtils.rm fn
114
177
  end
115
178
 
116
179
  unless project.added_issues.empty?
117
- puts "You may have to inform your SCM that the following files have been added:"
118
- project.added_issues.each { |i| puts " " + File.join(dir, ISSUE_TO_FN(i)) }
180
+ unless Ditz::HookManager.run :after_add, project, config, project.added_issues
181
+ puts "You may have to inform your SCM that the following files have been added:"
182
+ project.added_issues.each { |i| puts " " + i.pathname }
183
+ end
119
184
  end
120
185
 
121
186
  unless project.deleted_issues.empty?
122
- puts "You may have to inform your SCM that the following files have been deleted:"
123
- project.deleted_issues.each { |i| puts " " + File.join(dir, ISSUE_TO_FN(i)) }
187
+ unless Ditz::HookManager.run :after_delete, project, config, project.deleted_issues
188
+ puts "You may have to inform your SCM that the following files have been deleted:"
189
+ project.deleted_issues.each { |i| puts " " + i.pathname }
190
+ end
191
+ end
192
+
193
+ changed_not_added_issues = changed_issues - project.added_issues
194
+ unless changed_not_added_issues.empty?
195
+ Ditz::HookManager.run :after_update, project, config, changed_not_added_issues
124
196
  end
125
197
 
126
198
  config.save! $opts[:config_file] if config.changed?
File without changes
@@ -1,13 +1,29 @@
1
1
  module Ditz
2
2
 
3
- VERSION = "0.2"
3
+ VERSION = "0.3"
4
4
 
5
5
  def debug s
6
6
  puts "# #{s}" if $opts[:verbose]
7
7
  end
8
8
  module_function :debug
9
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
+ end
18
+
19
+ begin
20
+ Ditz::has_readline = false
21
+ require 'readline'
22
+ Ditz::has_readline = true
23
+ rescue LoadError
24
+ # do nothing
10
25
  end
11
26
 
12
27
  require 'model-objects'
13
28
  require 'operator'
29
+ require 'hook'
@@ -0,0 +1,60 @@
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
+ fns = File.join(ENV['HOME'], '.ditz', 'hooks', '*.rb')
54
+ Dir[fns].each { |fn| load fn }
55
+ end
56
+
57
+ @blocks[name] || []
58
+ end
59
+ end
60
+ end
@@ -25,6 +25,12 @@ class ErbHtml
25
25
  "<a href=\"#{dest}\">#{name}</a>"
26
26
  end
27
27
 
28
+ def link_issue_names project, s
29
+ project.issues.inject(s) do |s, i|
30
+ s.gsub(/\b#{i.name}\b/, link_to(i, i.title))
31
+ end
32
+ end
33
+
28
34
  def render template_name, morevars={}
29
35
  ErbHtml.new(@template_dir, template_name, @links, @mapping.merge(morevars)).to_s
30
36
  end
@@ -18,8 +18,7 @@
18
18
  issues = project.issues_for_release r
19
19
  num_done = issues.count_of { |i| i.closed? }
20
20
  pct_done = issues.size == 0 ? 100 : (100.0 * num_done / issues.size)
21
- num_bugs_todo = issues.count_of { |i| i.bug? && i.open? }
22
- num_feats_todo = issues.count_of { |i| i.feature? && i.open? }
21
+ open_issues = project.group_issues(issues.select { |i| i.open? })
23
22
  %>
24
23
  <li>
25
24
  <%= link_to r, "Release #{r.name}" %>:
@@ -27,8 +26,11 @@
27
26
  no issues
28
27
  <% else %>
29
28
  <%= sprintf "%.0f%%", pct_done %> complete;
30
- <%= "bug".pluralize num_bugs_todo %> and
31
- <%= "feature".pluralize num_feats_todo %> 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 %>
32
34
  <% end %>
33
35
  </li>
34
36
  <% end %>
@@ -52,7 +54,11 @@
52
54
  open_issues = issues.select { |i| i.open? }
53
55
  %>
54
56
  <p>
55
- <%= link_to "unassigned", "unassigned issue".pluralize(issues.size).ucfirst %>; <%= 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 %>
56
62
  </p>
57
63
 
58
64
  <% if components.size > 1 %>
@@ -60,17 +66,14 @@
60
66
  <ul>
61
67
  <% components.each do |c| %>
62
68
  <%
63
- open_issues = project.issues_for_component(c).select { |i| i.open? }
64
- num_bugs_todo = open_issues.count_of { |i| i.bug? }
65
- num_feats_todo = open_issues.count_of { |i| i.feature? }
69
+ open_issues = project.group_issues(project.issues_for_component(c).select { |i| i.open? })
66
70
  %>
67
71
  <li>
68
72
  <%= link_to c, c.name %>:
69
73
  <% if open_issues.empty? %>
70
74
  no open issues.
71
75
  <% else %>
72
- <%= "open bug".pluralize num_bugs_todo %> and
73
- <%= "open feature".pluralize num_feats_todo %>.
76
+ <%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
74
77
  <% end %>
75
78
  </li>
76
79
  <% end %>