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 +13 -0
- data/README.txt +6 -6
- data/Rakefile +2 -2
- data/ReleaseNotes +13 -0
- data/bin/ditz +116 -44
- data/bin/ditz-convert-from-monolith +0 -0
- data/lib/ditz.rb +17 -1
- data/lib/hook.rb +60 -0
- data/lib/html.rb +6 -0
- data/lib/index.rhtml +13 -10
- data/lib/issue.rhtml +3 -7
- data/lib/lowline.rb +40 -18
- data/lib/model-objects.rb +58 -18
- data/lib/model.rb +75 -14
- data/lib/operator.rb +182 -69
- data/lib/release.rhtml +1 -1
- data/lib/util.rb +8 -0
- metadata +51 -43
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
|
12
|
-
|
13
|
-
state is handled by version control like code
|
14
|
-
commit, merged with changes from other
|
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
|
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 "
|
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 "
|
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|
|
data/ReleaseNotes
CHANGED
@@ -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
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
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 "#{
|
26
|
-
|
27
|
-
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, #{
|
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
|
-
|
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 =
|
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 =
|
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 =
|
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.
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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 =
|
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
|
-
|
118
|
-
|
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
|
-
|
123
|
-
|
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
|
data/lib/ditz.rb
CHANGED
@@ -1,13 +1,29 @@
|
|
1
1
|
module Ditz
|
2
2
|
|
3
|
-
VERSION = "0.
|
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'
|
data/lib/hook.rb
ADDED
@@ -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
|
data/lib/html.rb
CHANGED
@@ -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
|
data/lib/index.rhtml
CHANGED
@@ -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
|
-
|
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
|
-
|
31
|
-
|
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
|
-
|
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
|
-
<%=
|
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 %>
|