ditz 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
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 %>