ditz 0.1.2 → 0.2
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 +7 -0
- data/Rakefile +1 -1
- data/ReleaseNotes +8 -0
- data/bin/ditz +69 -18
- data/bin/ditz-convert-from-monolith +42 -0
- data/lib/component.rhtml +1 -1
- data/lib/ditz.rb +1 -1
- data/lib/index.rhtml +1 -1
- data/lib/issue.rhtml +1 -1
- data/lib/lowline.rb +48 -11
- data/lib/model-objects.rb +11 -8
- data/lib/model.rb +9 -8
- data/lib/operator.rb +195 -120
- data/lib/release.rhtml +1 -1
- data/lib/unassigned.rhtml +1 -1
- data/lib/util.rb +11 -0
- metadata +5 -2
data/Changelog
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
== 0.2 / 2008-04-11
|
2
|
+
* bugfix: store each issue in a separate file to avoid false conflicts
|
3
|
+
* added per-command help
|
4
|
+
* added 'log' command for recent activity
|
5
|
+
* added better exception handling---turn into pretty error messages
|
6
|
+
* added text-area commands like /edit, /reset, etc
|
7
|
+
* all times now stored in UTC
|
1
8
|
== 0.1.2 / 2008-04-04
|
2
9
|
* bugfix: add_reference very broken
|
3
10
|
== 0.1.1 / 2008-04-04
|
data/Rakefile
CHANGED
@@ -42,7 +42,7 @@ end
|
|
42
42
|
|
43
43
|
task :upload_report do |t|
|
44
44
|
sh "ruby -Ilib bin/ditz html ditz"
|
45
|
-
sh "
|
45
|
+
sh "rsync -essh -cavz ditz wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
|
46
46
|
end
|
47
47
|
|
48
48
|
# vim: syntax=ruby
|
data/ReleaseNotes
ADDED
data/bin/ditz
CHANGED
@@ -1,35 +1,51 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
|
+
require 'fileutils'
|
4
5
|
require 'trollop'; include Trollop
|
5
6
|
require "ditz"
|
6
7
|
|
8
|
+
PROJECT_FN = "project.yaml"
|
9
|
+
CONFIG_FN = ".ditz-config"
|
10
|
+
def ISSUE_TO_FN i; "issue-#{i.id}.yaml" end
|
11
|
+
|
7
12
|
$opts = options do
|
8
|
-
version "ditz #{Ditz::VERSION}
|
9
|
-
opt :
|
10
|
-
opt :config_file, "Configuration file", :default => File.join(ENV["HOME"],
|
13
|
+
version "ditz #{Ditz::VERSION}"
|
14
|
+
opt :issue_dir, "Issue database dir", :default => "bugs"
|
15
|
+
opt :config_file, "Configuration file", :default => File.join(ENV["HOME"], CONFIG_FN)
|
11
16
|
opt :verbose, "Verbose output", :default => false
|
12
17
|
end
|
13
18
|
|
14
19
|
cmd = ARGV.shift or die "expecting a ditz command"
|
15
20
|
op = Ditz::Operator.new
|
21
|
+
dir = $opts[:issue_dir]
|
16
22
|
|
17
|
-
case cmd # special cases
|
23
|
+
case cmd # some special cases not handled by Ditz::Operator
|
18
24
|
when "init"
|
19
|
-
|
20
|
-
|
25
|
+
die "#{dir} directory already exists" if File.exists? dir
|
26
|
+
FileUtils.mkdir dir
|
27
|
+
fn = File.join dir, PROJECT_FN
|
21
28
|
project = op.init
|
22
29
|
project.save! fn
|
23
|
-
puts "Ok, #{
|
30
|
+
puts "Ok, #{dir} directory created successfully."
|
24
31
|
exit
|
25
32
|
when "help"
|
26
|
-
op.help
|
33
|
+
op.do "help", nil, nil, ARGV
|
27
34
|
exit
|
28
35
|
end
|
29
36
|
|
30
|
-
|
37
|
+
die "No #{dir} directory---use 'ditz init' to initialize" unless File.exists? dir
|
38
|
+
|
31
39
|
project = begin
|
32
|
-
|
40
|
+
fn = File.join dir, PROJECT_FN
|
41
|
+
Ditz::debug "loading project from #{fn}"
|
42
|
+
project = Ditz::Project.from fn
|
43
|
+
|
44
|
+
fn = File.join dir, "issue-*.yaml"
|
45
|
+
Ditz::debug "loading issues from #{fn}"
|
46
|
+
project.issues = Dir[fn].map { |fn| Ditz::Issue.from fn }
|
47
|
+
Ditz::debug "found #{project.issues.size} issues"
|
48
|
+
project
|
33
49
|
rescue SystemCallError, Ditz::Project::Error => e
|
34
50
|
die "#{e.message} (use 'init' to initialize)"
|
35
51
|
end
|
@@ -39,12 +55,11 @@ project.assign_issue_names!
|
|
39
55
|
project.each_modelobject { |o| o.after_deserialize project }
|
40
56
|
|
41
57
|
config = begin
|
42
|
-
|
43
|
-
|
44
|
-
Ditz::
|
45
|
-
Ditz::Config.from fn
|
58
|
+
if File.exists? CONFIG_FN
|
59
|
+
Ditz::debug "loading local config from #{CONFIG_FN}"
|
60
|
+
Ditz::Config.from CONFIG_FN
|
46
61
|
else
|
47
|
-
Ditz::debug "loading config from #{$opts[:config_file]}"
|
62
|
+
Ditz::debug "loading global config from #{$opts[:config_file]}"
|
48
63
|
Ditz::Config.from $opts[:config_file]
|
49
64
|
end
|
50
65
|
rescue SystemCallError, Ditz::ModelObject::ModelError => e
|
@@ -64,14 +79,50 @@ args = []
|
|
64
79
|
args << ARGV.shift until ARGV.empty?
|
65
80
|
|
66
81
|
Ditz::debug "executing command #{cmd}"
|
67
|
-
|
82
|
+
begin
|
83
|
+
op.do cmd, project, config, args
|
84
|
+
rescue Ditz::Operator::Error => e
|
85
|
+
die e.message
|
86
|
+
rescue Interrupt
|
87
|
+
exit 1
|
88
|
+
end
|
68
89
|
|
90
|
+
## save project.yaml
|
69
91
|
dirty = project.each_modelobject { |o| break true if o.changed? } || false
|
70
92
|
if dirty
|
71
|
-
|
93
|
+
fn = File.join dir, PROJECT_FN
|
94
|
+
Ditz::debug "project is dirty, saving #{fn}"
|
72
95
|
project.each_modelobject { |o| o.before_serialize project }
|
73
|
-
project.save!
|
96
|
+
project.save! fn
|
97
|
+
end
|
98
|
+
|
99
|
+
## project issues are not model fields proper, so they must be
|
100
|
+
## 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
|
74
108
|
end
|
109
|
+
|
110
|
+
project.deleted_issues.each do |i|
|
111
|
+
fn = File.join dir, ISSUE_TO_FN(i)
|
112
|
+
Ditz::debug "issue #{i.name} has been deleted, deleting #{fn}"
|
113
|
+
FileUtils.rm fn
|
114
|
+
end
|
115
|
+
|
116
|
+
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)) }
|
119
|
+
end
|
120
|
+
|
121
|
+
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)) }
|
124
|
+
end
|
125
|
+
|
75
126
|
config.save! $opts[:config_file] if config.changed?
|
76
127
|
|
77
128
|
# vim: syntax=ruby
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'fileutils'
|
5
|
+
require "ditz"
|
6
|
+
|
7
|
+
PROJECT_FN = "project.yaml"
|
8
|
+
CONFIG_FN = ".ditz-config"
|
9
|
+
def ISSUE_TO_FN i; "issue-#{i.id}.yaml" end
|
10
|
+
|
11
|
+
dir = "bugs"
|
12
|
+
project = Ditz::Project.from "bugs.yaml"
|
13
|
+
puts "making #{dir}"
|
14
|
+
FileUtils.mkdir dir
|
15
|
+
project.changed!
|
16
|
+
project.issues.each { |i| i.changed! }
|
17
|
+
|
18
|
+
project.validate!
|
19
|
+
project.assign_issue_names!
|
20
|
+
project.each_modelobject { |o| o.after_deserialize project }
|
21
|
+
|
22
|
+
## save project.yaml
|
23
|
+
dirty = project.each_modelobject { |o| break true if o.changed? } || false
|
24
|
+
if dirty
|
25
|
+
fn = File.join dir, PROJECT_FN
|
26
|
+
puts "writing #{fn}"
|
27
|
+
project.each_modelobject { |o| o.before_serialize project }
|
28
|
+
project.save! fn
|
29
|
+
end
|
30
|
+
|
31
|
+
## project issues are not model fields proper, so they must be
|
32
|
+
## saved independently.
|
33
|
+
project.issues.each do |i|
|
34
|
+
if i.changed?
|
35
|
+
i.before_serialize project
|
36
|
+
fn = File.join dir, ISSUE_TO_FN(i)
|
37
|
+
puts "writing #{fn}"
|
38
|
+
i.save! fn
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
puts "You can delete bugs.yaml now."
|
data/lib/component.rhtml
CHANGED
@@ -13,6 +13,6 @@
|
|
13
13
|
|
14
14
|
<%= render "issue_table", :show_component => false, :show_release => true %>
|
15
15
|
|
16
|
-
<p class="footer">Generated
|
16
|
+
<p class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.
|
17
17
|
</body>
|
18
18
|
</html>
|
data/lib/ditz.rb
CHANGED
data/lib/index.rhtml
CHANGED
data/lib/issue.rhtml
CHANGED
data/lib/lowline.rb
CHANGED
@@ -1,15 +1,16 @@
|
|
1
|
-
require '
|
1
|
+
require 'tempfile'
|
2
|
+
require "util"
|
2
3
|
|
3
4
|
class Numeric
|
4
5
|
def to_pretty_s
|
5
|
-
|
6
|
-
%w(no one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen)[self]
|
7
|
-
else
|
8
|
-
to_s
|
9
|
-
end
|
6
|
+
%w(no one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen)[self] || to_s
|
10
7
|
end
|
11
8
|
end
|
12
9
|
|
10
|
+
class NilClass
|
11
|
+
def multiline prefix=nil; "" end
|
12
|
+
end
|
13
|
+
|
13
14
|
class String
|
14
15
|
def ucfirst; self[0..0].upcase + self[1..-1] end
|
15
16
|
def dcfirst; self[0..0].downcase + self[1..-1] end
|
@@ -17,6 +18,7 @@ class String
|
|
17
18
|
def underline; self + "\n" + ("-" * self.length) end
|
18
19
|
def multiline prefix=""; blank? ? "" : "\n" + self.gsub(/^/, prefix) end
|
19
20
|
def pluralize n; n.to_pretty_s + " " + (n == 1 ? self : self + "s") end # oh yeah
|
21
|
+
def multistrip; strip.gsub(/\n\n+/, "\n\n") end
|
20
22
|
end
|
21
23
|
|
22
24
|
class Array
|
@@ -52,6 +54,20 @@ class Time
|
|
52
54
|
end
|
53
55
|
|
54
56
|
module Lowline
|
57
|
+
def run_editor
|
58
|
+
f = Tempfile.new "ditz"
|
59
|
+
yield f
|
60
|
+
f.close
|
61
|
+
|
62
|
+
editor = ENV["EDITOR"] || "/usr/bin/vi"
|
63
|
+
cmd = "#{editor} #{f.path.inspect}"
|
64
|
+
|
65
|
+
mtime = File.mtime f.path
|
66
|
+
system cmd or raise Error, "cannot execute command: #{cmd.inspect}"
|
67
|
+
|
68
|
+
File.mtime(f.path) == mtime ? nil : f.path
|
69
|
+
end
|
70
|
+
|
55
71
|
def ask q, opts={}
|
56
72
|
default_s = case opts[:default]
|
57
73
|
when nil; nil
|
@@ -77,16 +93,35 @@ module Lowline
|
|
77
93
|
end
|
78
94
|
end
|
79
95
|
|
96
|
+
def ask_via_editor q, default=nil
|
97
|
+
fn = run_editor do |f|
|
98
|
+
f.puts q.gsub(/^/, "## ")
|
99
|
+
f.puts "##"
|
100
|
+
f.puts "## Enter your text below. Lines starting with a '#' will be ignored."
|
101
|
+
f.puts
|
102
|
+
f.puts default if default
|
103
|
+
end
|
104
|
+
return unless fn
|
105
|
+
IO.read(fn).gsub(/^#.*$/, "").multistrip
|
106
|
+
end
|
107
|
+
|
80
108
|
def ask_multiline q
|
81
|
-
puts "#{q} (ctrl-d or
|
109
|
+
puts "#{q} (ctrl-d, ., or /stop to stop, /edit to edit, /reset to reset):"
|
82
110
|
ans = ""
|
83
111
|
while true
|
84
112
|
print "> "
|
85
|
-
line = gets
|
86
|
-
|
87
|
-
|
113
|
+
case(line = gets) && line.strip!
|
114
|
+
when /^\.$/, nil, "/stop"
|
115
|
+
break
|
116
|
+
when "/reset"
|
117
|
+
return ask_multiline(q)
|
118
|
+
when "/edit"
|
119
|
+
return ask_via_editor(q, ans)
|
120
|
+
else
|
121
|
+
ans << line + "\n"
|
122
|
+
end
|
88
123
|
end
|
89
|
-
ans.
|
124
|
+
ans.multistrip
|
90
125
|
end
|
91
126
|
|
92
127
|
def ask_yon q
|
@@ -129,6 +164,8 @@ module Lowline
|
|
129
164
|
puts "Choose a #{name}:"
|
130
165
|
stuff.each_with_index do |c, i|
|
131
166
|
pretty = case to_string
|
167
|
+
when block_given? && to_string # heh
|
168
|
+
yield c
|
132
169
|
when Symbol
|
133
170
|
c.send to_string
|
134
171
|
when Proc
|
data/lib/model-objects.rb
CHANGED
@@ -38,10 +38,16 @@ class Project < ModelObject
|
|
38
38
|
|
39
39
|
field :name, :default_generator => lambda { File.basename(Dir.pwd) }
|
40
40
|
field :version, :default => Ditz::VERSION, :ask => false
|
41
|
-
field :issues, :multi => true, :ask => false
|
42
41
|
field :components, :multi => true, :generator => :get_components
|
43
42
|
field :releases, :multi => true, :ask => false
|
44
43
|
|
44
|
+
## issues are not model fields proper, so we build up their interface here.
|
45
|
+
attr_accessor :issues
|
46
|
+
def add_issue issue; added_issues << issue; issues << issue end
|
47
|
+
def drop_issue issue; deleted_issues << issue if issues.delete issue end
|
48
|
+
def added_issues; @added_issues ||= [] end
|
49
|
+
def deleted_issues; @deleted_issues ||= [] end
|
50
|
+
|
45
51
|
def get_components
|
46
52
|
puts <<EOS
|
47
53
|
Issues can be tracked across the project as a whole, or the project can be
|
@@ -54,18 +60,15 @@ EOS
|
|
54
60
|
end
|
55
61
|
|
56
62
|
def issue_for issue_name
|
57
|
-
issues.find { |i| i.name == issue_name }
|
58
|
-
raise Error, "has no issue with name #{issue_name.inspect}"
|
63
|
+
issues.find { |i| i.name == issue_name }
|
59
64
|
end
|
60
65
|
|
61
66
|
def component_for component_name
|
62
|
-
components.find { |i| i.name == component_name }
|
63
|
-
raise Error, "has no component with name #{component_name.inspect}"
|
67
|
+
components.find { |i| i.name == component_name }
|
64
68
|
end
|
65
69
|
|
66
70
|
def release_for release_name
|
67
|
-
releases.find { |i| i.name == release_name }
|
68
|
-
raise Error, "has no release with name #{release_name.inspect}"
|
71
|
+
releases.find { |i| i.name == release_name }
|
69
72
|
end
|
70
73
|
|
71
74
|
def issues_for_release release
|
@@ -83,7 +86,7 @@ EOS
|
|
83
86
|
def assign_issue_names!
|
84
87
|
prefixes = components.map { |c| [c.name, c.name.gsub(/^\s+/, "-").downcase] }.to_h
|
85
88
|
ids = components.map { |c| [c.name, 0] }.to_h
|
86
|
-
issues.each do |i|
|
89
|
+
issues.sort_by { |i| i.creation_time }.each do |i|
|
87
90
|
i.name = "#{prefixes[i.component]}-#{ids[i.component] += 1}"
|
88
91
|
end
|
89
92
|
end
|
data/lib/model.rb
CHANGED
@@ -3,6 +3,13 @@ require "lowline"; include Lowline
|
|
3
3
|
require "util"
|
4
4
|
require 'sha1'
|
5
5
|
|
6
|
+
class Time
|
7
|
+
alias :old_to_yaml :to_yaml
|
8
|
+
def to_yaml(opts = {})
|
9
|
+
self.utc.old_to_yaml(opts)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
6
13
|
module Ditz
|
7
14
|
|
8
15
|
class ModelObject
|
@@ -82,8 +89,7 @@ class ModelObject
|
|
82
89
|
end
|
83
90
|
|
84
91
|
def save! fn
|
85
|
-
|
86
|
-
FileUtils.mv fn, "#{fn}~", :force => true rescue nil
|
92
|
+
#FileUtils.mv fn, "#{fn}~", :force => true rescue nil
|
87
93
|
File.open(fn, "w") { |f| f.puts to_yaml }
|
88
94
|
end
|
89
95
|
|
@@ -92,12 +98,7 @@ class ModelObject
|
|
92
98
|
self
|
93
99
|
end
|
94
100
|
|
95
|
-
def
|
96
|
-
@changed = false
|
97
|
-
@log_events = []
|
98
|
-
end
|
99
|
-
|
100
|
-
def changed?; @changed end
|
101
|
+
def changed?; @changed ||= false end
|
101
102
|
def changed!; @changed = true end
|
102
103
|
|
103
104
|
def self.create_interactively opts={}
|
data/lib/operator.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
|
-
require 'tempfile'
|
2
1
|
require 'fileutils'
|
3
|
-
|
4
2
|
require "html"
|
5
3
|
|
6
4
|
module Ditz
|
@@ -12,18 +10,82 @@ class Operator
|
|
12
10
|
def method_to_op meth; meth.to_s.gsub("_", "-") end
|
13
11
|
def op_to_method op; op.gsub("-", "_").intern end
|
14
12
|
|
15
|
-
def operation method, desc
|
13
|
+
def operation method, desc, *args_spec
|
16
14
|
@operations ||= {}
|
17
|
-
@operations[method] = desc
|
15
|
+
@operations[method] = { :desc => desc, :args_spec => args_spec }
|
18
16
|
end
|
19
17
|
|
20
18
|
def operations
|
21
19
|
@operations.map { |k, v| [method_to_op(k), v] }.sort_by { |k, v| k }
|
22
20
|
end
|
23
21
|
def has_operation? op; @operations.member? op_to_method(op) end
|
22
|
+
|
23
|
+
def parse_releases_arg project, releases_arg
|
24
|
+
ret = []
|
25
|
+
|
26
|
+
releases, show_unassigned, force_show = case releases_arg
|
27
|
+
when nil; [project.releases, true, false]
|
28
|
+
when "unassigned"; [[], true, true]
|
29
|
+
else
|
30
|
+
release = project.release_for(releases_arg)
|
31
|
+
raise Error, "no release with name #{releases_arg}" unless release
|
32
|
+
[[release], false, true]
|
33
|
+
end
|
34
|
+
|
35
|
+
releases.each do |r|
|
36
|
+
next if r.released? unless force_show
|
37
|
+
|
38
|
+
bugs = project.issues.
|
39
|
+
select { |i| i.type == :bugfix && i.release == r.name }
|
40
|
+
feats = project.issues.
|
41
|
+
select { |i| i.type == :feature && i.release == r.name }
|
42
|
+
|
43
|
+
#next if bugs.empty? && feats.empty? unless force_show
|
44
|
+
|
45
|
+
ret << [r, bugs, feats]
|
46
|
+
end
|
47
|
+
|
48
|
+
return ret unless show_unassigned
|
49
|
+
|
50
|
+
bugs = project.issues.select { |i| i.type == :bugfix && i.release.nil? }
|
51
|
+
feats = project.issues.select { |i| i.type == :feature && i.release.nil? }
|
52
|
+
|
53
|
+
return ret if bugs.empty? && feats.empty? unless force_show
|
54
|
+
ret << [nil, bugs, feats]
|
55
|
+
end
|
56
|
+
private :parse_releases_arg
|
57
|
+
|
58
|
+
def build_args project, method, args
|
59
|
+
command = "command '#{method_to_op method}'"
|
60
|
+
built_args = @operations[method][:args_spec].map do |spec|
|
61
|
+
val = args.shift
|
62
|
+
case spec
|
63
|
+
when :issue
|
64
|
+
raise Error, "#{command} requires an issue name" unless val
|
65
|
+
project.issue_for(val) or raise Error, "no issue with name #{val}"
|
66
|
+
when :release
|
67
|
+
raise Error, "#{command} requires a release name" unless val
|
68
|
+
project.release_for(val) or raise Error, "no release with name #{val}"
|
69
|
+
when :maybe_release
|
70
|
+
parse_releases_arg project, val
|
71
|
+
when :string
|
72
|
+
raise Error, "#{command} requires a string" unless val
|
73
|
+
val
|
74
|
+
else
|
75
|
+
val # no translation for other types
|
76
|
+
end
|
77
|
+
end
|
78
|
+
raise Error, "too many arguments for #{command}" unless args.empty?
|
79
|
+
built_args
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def do op, project, config, args
|
84
|
+
meth = self.class.op_to_method(op)
|
85
|
+
built_args = self.class.build_args project, meth, args
|
86
|
+
send meth, project, config, *built_args
|
24
87
|
end
|
25
88
|
|
26
|
-
def do op, *a; send self.class.op_to_method(op), *a end
|
27
89
|
%w(operations has_operation?).each do |m|
|
28
90
|
define_method(m) { |*a| self.class.send m, *a }
|
29
91
|
end
|
@@ -33,15 +95,40 @@ class Operator
|
|
33
95
|
Project.create_interactively
|
34
96
|
end
|
35
97
|
|
36
|
-
operation :help, "List all registered commands"
|
37
|
-
def help
|
98
|
+
operation :help, "List all registered commands", :maybe_command
|
99
|
+
def help project, config, command
|
100
|
+
return help_single(command) if command
|
38
101
|
puts <<EOS
|
39
|
-
|
102
|
+
Ditz commands:
|
103
|
+
|
40
104
|
EOS
|
41
105
|
ops = self.class.operations
|
42
|
-
len = ops.map { |name,
|
43
|
-
ops.each
|
44
|
-
|
106
|
+
len = ops.map { |name, op| name.to_s.length }.max
|
107
|
+
ops.each do |name, opts|
|
108
|
+
printf " %#{len}s: %s\n", name, opts[:desc]
|
109
|
+
end
|
110
|
+
puts <<EOS
|
111
|
+
|
112
|
+
Use 'ditz help <command>' for details.
|
113
|
+
EOS
|
114
|
+
end
|
115
|
+
|
116
|
+
def help_single command
|
117
|
+
name, opts = self.class.operations.find { |name, spec| name == command }
|
118
|
+
raise Error, "no such ditz command '#{command}'" unless name
|
119
|
+
args = opts[:args_spec].map do |spec|
|
120
|
+
case spec.to_s
|
121
|
+
when /^maybe_(.*)$/
|
122
|
+
"[#{$1}]"
|
123
|
+
else
|
124
|
+
"<#{spec.to_s}>"
|
125
|
+
end
|
126
|
+
end.join(" ")
|
127
|
+
|
128
|
+
puts <<EOS
|
129
|
+
#{opts[:desc]}.
|
130
|
+
Usage: ditz #{name} #{args}
|
131
|
+
EOS
|
45
132
|
end
|
46
133
|
|
47
134
|
operation :add, "Add a bug/feature request"
|
@@ -54,9 +141,8 @@ EOS
|
|
54
141
|
puts "Added issue #{issue.name}."
|
55
142
|
end
|
56
143
|
|
57
|
-
operation :drop, "Drop a bug/feature request"
|
58
|
-
def drop project, config,
|
59
|
-
issue = project.issue_for issue_name
|
144
|
+
operation :drop, "Drop a bug/feature request", :issue
|
145
|
+
def drop project, config, issue
|
60
146
|
project.drop_issue issue
|
61
147
|
puts "Dropped #{issue.name}. Note that other issue names may have changed."
|
62
148
|
end
|
@@ -77,56 +163,19 @@ EOS
|
|
77
163
|
puts "Added component #{component.name}."
|
78
164
|
end
|
79
165
|
|
80
|
-
operation :add_reference, "Add a reference to an issue"
|
81
|
-
def add_reference project, config,
|
82
|
-
|
166
|
+
operation :add_reference, "Add a reference to an issue", :issue
|
167
|
+
def add_reference project, config, issue
|
168
|
+
puts "Adding a reference to #{issue.name}: #{issue.title}."
|
83
169
|
reference = ask "Reference"
|
84
170
|
comment = ask_multiline "Comments"
|
85
171
|
issue.add_reference reference
|
86
172
|
issue.log "added reference #{issue.references.size}", config.user, comment
|
87
|
-
puts "Added reference to #{issue.name}"
|
88
|
-
end
|
89
|
-
|
90
|
-
def parse_releases_arg project, releases_arg
|
91
|
-
ret = []
|
92
|
-
|
93
|
-
releases, show_unassigned, force_show = case releases_arg
|
94
|
-
when nil; [project.releases, true, false]
|
95
|
-
when "unassigned"; [[], true, true]
|
96
|
-
else
|
97
|
-
[[project.release_for(releases_arg)], false, true]
|
98
|
-
end
|
99
|
-
|
100
|
-
releases.each do |r|
|
101
|
-
next if r.released? unless force_show
|
102
|
-
|
103
|
-
bugs = project.issues.
|
104
|
-
select { |i| i.type == :bugfix && i.release == r.name }
|
105
|
-
feats = project.issues.
|
106
|
-
select { |i| i.type == :feature && i.release == r.name }
|
107
|
-
|
108
|
-
#next if bugs.empty? && feats.empty? unless force_show
|
109
|
-
|
110
|
-
ret << [r, bugs, feats]
|
111
|
-
end
|
112
|
-
|
113
|
-
return ret unless show_unassigned
|
114
|
-
|
115
|
-
bugs = project.issues.select { |i| i.type == :bugfix && i.release.nil? }
|
116
|
-
feats = project.issues.select { |i| i.type == :feature && i.release.nil? }
|
117
|
-
|
118
|
-
return ret if bugs.empty? && feats.empty? unless force_show
|
119
|
-
ret << [nil, bugs, feats]
|
173
|
+
puts "Added reference to #{issue.name}."
|
120
174
|
end
|
121
175
|
|
122
|
-
operation :status, "Show project status"
|
123
|
-
def status project, config,
|
124
|
-
|
125
|
-
puts "No releases."
|
126
|
-
return
|
127
|
-
end
|
128
|
-
|
129
|
-
parse_releases_arg(project, release).each do |r, bugs, feats|
|
176
|
+
operation :status, "Show project status", :maybe_release
|
177
|
+
def status project, config, releases
|
178
|
+
releases.each do |r, bugs, feats|
|
130
179
|
title, bar = [r ? r.name : "unassigned", status_bar_for(bugs + feats)]
|
131
180
|
|
132
181
|
ncbugs = bugs.count_of { |b| b.closed? }
|
@@ -134,7 +183,9 @@ EOS
|
|
134
183
|
pcbugs = 100.0 * (bugs.empty? ? 1.0 : ncbugs.to_f / bugs.size)
|
135
184
|
pcfeats = 100.0 * (feats.empty? ? 1.0 : ncfeats.to_f / feats.size)
|
136
185
|
|
137
|
-
special = if
|
186
|
+
special = if r && r.released?
|
187
|
+
"(released)"
|
188
|
+
elsif bugs.empty? && feats.empty?
|
138
189
|
"(no issues)"
|
139
190
|
elsif ncbugs == bugs.size && ncfeats == feats.size
|
140
191
|
"(ready for release)"
|
@@ -145,6 +196,11 @@ EOS
|
|
145
196
|
printf "%-10s %2d/%2d (%3.0f%%) bugs, %2d/%2d (%3.0f%%) features %s\n",
|
146
197
|
title, ncbugs, bugs.size, pcbugs, ncfeats, feats.size, pcfeats, special
|
147
198
|
end
|
199
|
+
|
200
|
+
if project.releases.empty?
|
201
|
+
puts "No releases."
|
202
|
+
return
|
203
|
+
end
|
148
204
|
end
|
149
205
|
|
150
206
|
def status_bar_for issues
|
@@ -155,24 +211,25 @@ EOS
|
|
155
211
|
end
|
156
212
|
|
157
213
|
def todo_list_for issues
|
214
|
+
return if issues.empty?
|
158
215
|
name_len = issues.max_of { |i| i.name.length }
|
159
216
|
issues.map do |i|
|
160
217
|
sprintf "%s %#{name_len}s: %s\n", i.status_widget, i.name, i.title
|
161
218
|
end.join
|
162
219
|
end
|
163
220
|
|
164
|
-
operation :todo, "Generate todo list"
|
165
|
-
def todo project, config,
|
166
|
-
actually_do_todo project, config,
|
221
|
+
operation :todo, "Generate todo list", :maybe_release
|
222
|
+
def todo project, config, releases
|
223
|
+
actually_do_todo project, config, releases, false
|
167
224
|
end
|
168
225
|
|
169
|
-
operation :todo_full, "Generate full todo list, including completed items"
|
170
|
-
def todo_full project, config,
|
171
|
-
actually_do_todo project, config,
|
226
|
+
operation :todo_full, "Generate full todo list, including completed items", :maybe_release
|
227
|
+
def todo_full project, config, releases
|
228
|
+
actually_do_todo project, config, releases, true
|
172
229
|
end
|
173
230
|
|
174
|
-
def actually_do_todo project, config,
|
175
|
-
|
231
|
+
def actually_do_todo project, config, releases, full
|
232
|
+
releases.each do |r, bugs, feats|
|
176
233
|
if r
|
177
234
|
puts "Version #{r.name} (#{r.status}):"
|
178
235
|
else
|
@@ -180,14 +237,13 @@ EOS
|
|
180
237
|
end
|
181
238
|
issues = bugs + feats
|
182
239
|
issues = issues.select { |i| i.open? } unless full
|
183
|
-
|
240
|
+
puts(todo_list_for(issues.sort_by { |i| i.sort_order }) || "No open issues.")
|
184
241
|
puts
|
185
242
|
end
|
186
243
|
end
|
187
244
|
|
188
|
-
operation :show, "Describe a single issue"
|
189
|
-
def show project, config,
|
190
|
-
issue = project.issue_for name
|
245
|
+
operation :show, "Describe a single issue", :issue
|
246
|
+
def show project, config, issue
|
191
247
|
status = case issue.status
|
192
248
|
when :closed
|
193
249
|
"#{issue.status_string}: #{issue.disposition_string}"
|
@@ -198,11 +254,13 @@ EOS
|
|
198
254
|
#{"Issue #{issue.name}".underline}
|
199
255
|
Title: #{issue.title}
|
200
256
|
Description: #{issue.interpolated_desc(project.issues).multiline " "}
|
257
|
+
Type: #{issue.type}
|
201
258
|
Status: #{status}
|
202
259
|
Creator: #{issue.reporter}
|
203
260
|
Age: #{issue.creation_time.ago}
|
204
261
|
Release: #{issue.release}
|
205
262
|
References: #{issue.references.listify " "}
|
263
|
+
Identifier: #{issue.id}
|
206
264
|
|
207
265
|
Event log:
|
208
266
|
#{format_log_events issue.log_events}
|
@@ -216,25 +274,24 @@ EOS
|
|
216
274
|
end.join("\n")
|
217
275
|
end
|
218
276
|
|
219
|
-
operation :start, "Start work on an issue"
|
220
|
-
def start project, config,
|
221
|
-
issue
|
277
|
+
operation :start, "Start work on an issue", :issue
|
278
|
+
def start project, config, issue
|
279
|
+
puts "Starting work on issue #{issue.name}: #{issue.title}."
|
222
280
|
comment = ask_multiline "Comments"
|
223
281
|
issue.start_work config.user, comment
|
224
282
|
puts "Recorded start of work for #{issue.name}."
|
225
283
|
end
|
226
284
|
|
227
|
-
operation :stop, "Stop work on an issue"
|
228
|
-
def stop project, config,
|
229
|
-
issue
|
285
|
+
operation :stop, "Stop work on an issue", :issue
|
286
|
+
def stop project, config, issue
|
287
|
+
puts "Stopping work on issue #{issue.name}: #{issue.title}."
|
230
288
|
comment = ask_multiline "Comments"
|
231
289
|
issue.stop_work config.user, comment
|
232
290
|
puts "Recorded work stop for #{issue.name}."
|
233
291
|
end
|
234
292
|
|
235
|
-
operation :close, "Close an issue"
|
236
|
-
def close project, config,
|
237
|
-
issue = project.issue_for name
|
293
|
+
operation :close, "Close an issue", :issue
|
294
|
+
def close project, config, issue
|
238
295
|
puts "Closing issue #{issue.name}: #{issue.title}."
|
239
296
|
disp = ask_for_selection Issue::DISPOSITIONS, "disposition", lambda { |x| Issue::DISPOSITION_STRINGS[x] || x.to_s }
|
240
297
|
comment = ask_multiline "Comments"
|
@@ -242,31 +299,39 @@ EOS
|
|
242
299
|
puts "Closed issue #{issue.name} with disposition #{issue.disposition_string}."
|
243
300
|
end
|
244
301
|
|
245
|
-
operation :assign, "Assign an issue to a release"
|
246
|
-
def assign project, config,
|
247
|
-
issue = project.issue_for issue_name
|
302
|
+
operation :assign, "Assign an issue to a release", :issue
|
303
|
+
def assign project, config, issue
|
248
304
|
puts "Issue #{issue.name} currently " + if issue.release
|
249
305
|
"assigned to release #{issue.release}."
|
250
306
|
else
|
251
307
|
"not assigned to any release."
|
252
308
|
end
|
253
|
-
|
309
|
+
|
310
|
+
releases = project.releases.sort_by { |r| (r.release_time || 0).to_i }
|
311
|
+
releases -= [releases.find { |r| r.name == issue.release }] if issue.release
|
312
|
+
release = ask_for_selection(releases, "release") do |r|
|
313
|
+
r.name + if r.released?
|
314
|
+
" (released #{r.release_time.pretty_date})"
|
315
|
+
else
|
316
|
+
" (unreleased)"
|
317
|
+
end
|
318
|
+
end
|
254
319
|
comment = ask_multiline "Comments"
|
255
320
|
issue.assign_to_release release, config.user, comment
|
256
|
-
puts "Assigned #{issue.name} to #{release.name}"
|
321
|
+
puts "Assigned #{issue.name} to #{release.name}."
|
257
322
|
end
|
258
323
|
|
259
|
-
operation :unassign, "Unassign an issue from any releases"
|
260
|
-
def unassign project, config,
|
261
|
-
issue
|
324
|
+
operation :unassign, "Unassign an issue from any releases", :issue
|
325
|
+
def unassign project, config, issue
|
326
|
+
puts "Unassigning issue #{issue.name}: #{issue.title}."
|
262
327
|
comment = ask_multiline "Comments"
|
263
328
|
issue.unassign config.user, comment
|
264
329
|
puts "Unassigned #{issue.name}."
|
265
330
|
end
|
266
331
|
|
267
|
-
operation :comment, "Comment on an issue"
|
268
|
-
def comment project, config,
|
269
|
-
issue
|
332
|
+
operation :comment, "Comment on an issue", :issue
|
333
|
+
def comment project, config, issue
|
334
|
+
puts "Commenting on issue #{issue.name}: #{issue.title}."
|
270
335
|
comment = ask_multiline "Comments"
|
271
336
|
issue.log "commented", config.user, comment
|
272
337
|
puts "Comments recorded for #{issue.name}."
|
@@ -274,32 +339,31 @@ EOS
|
|
274
339
|
|
275
340
|
operation :releases, "Show releases"
|
276
341
|
def releases project, config
|
277
|
-
project.releases.
|
342
|
+
a, b = project.releases.partition { |r| r.released? }
|
343
|
+
(b + a.sort_by { |r| r.release_time }).each do |r|
|
278
344
|
status = r.released? ? "released #{r.release_time.pretty_date}" : r.status
|
279
345
|
puts "#{r.name} (#{status})"
|
280
346
|
end
|
281
347
|
end
|
282
348
|
|
283
|
-
operation :release, "Release a release"
|
284
|
-
def release project, config,
|
285
|
-
release = project.release_for release_name
|
349
|
+
operation :release, "Release a release", :release
|
350
|
+
def release project, config, release
|
286
351
|
comment = ask_multiline "Comments"
|
287
352
|
release.release! project, config.user, comment
|
288
353
|
puts "Release #{release.name} released!"
|
289
354
|
end
|
290
355
|
|
291
|
-
operation :changelog, "Generate a changelog for a release"
|
292
|
-
def changelog project, config,
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
end
|
356
|
+
operation :changelog, "Generate a changelog for a release", :release
|
357
|
+
def changelog project, config, r
|
358
|
+
feats, bugs = project.issues_for_release(r).partition { |i| i.feature? }
|
359
|
+
puts "== #{r.name} / #{r.released? ? r.release_time.pretty_date : 'unreleased'}"
|
360
|
+
feats.select { |f| f.closed? }.each { |i| puts "* #{i.title}" }
|
361
|
+
bugs.select { |f| f.closed? }.each { |i| puts "* bugfix: #{i.title}" }
|
298
362
|
end
|
299
363
|
|
300
|
-
operation :html, "Generate html status pages"
|
301
|
-
def html project, config, dir
|
302
|
-
|
364
|
+
operation :html, "Generate html status pages", :maybe_dir
|
365
|
+
def html project, config, dir
|
366
|
+
dir ||= "html"
|
303
367
|
Dir.mkdir dir unless File.exists? dir
|
304
368
|
|
305
369
|
## find the ERB templates. this is my brilliant approach
|
@@ -360,6 +424,7 @@ EOS
|
|
360
424
|
:past_releases => past_rels, :upcoming_releases => upcoming_rels,
|
361
425
|
:components => project.components)
|
362
426
|
end
|
427
|
+
puts "Local generated URL: file://#{File.expand_path(fn)}"
|
363
428
|
end
|
364
429
|
|
365
430
|
operation :validate, "Validate project status"
|
@@ -367,36 +432,46 @@ EOS
|
|
367
432
|
## a no-op
|
368
433
|
end
|
369
434
|
|
370
|
-
operation :grep, "Show issues matching a string or regular expression"
|
435
|
+
operation :grep, "Show issues matching a string or regular expression", :string
|
371
436
|
def grep project, config, match
|
372
437
|
re = /#{match}/
|
373
438
|
issues = project.issues.select { |i| i.title =~ re || i.desc =~ re }
|
374
|
-
|
439
|
+
puts(todo_list_for(issues) || "No matching issues.")
|
375
440
|
end
|
376
441
|
|
377
|
-
operation :
|
378
|
-
def
|
379
|
-
|
442
|
+
operation :log, "Show recent activity"
|
443
|
+
def log project, config
|
444
|
+
project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
|
445
|
+
flatten_one_level.sort_by { |e| e.first.first }.reverse.
|
446
|
+
each do |(date, author, what, comment), i|
|
447
|
+
puts <<EOS
|
448
|
+
date : #{date.localtime} (#{date.ago} ago)
|
449
|
+
author: #{author}
|
450
|
+
|
451
|
+
#{i.name}: #{i.title}
|
452
|
+
#{what}
|
453
|
+
#{comment.multiline " "}
|
454
|
+
EOS
|
455
|
+
puts unless comment.blank?
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
operation :edit, "Edit an issue", :issue
|
460
|
+
def edit project, config, issue
|
380
461
|
data = { :title => issue.title, :description => issue.desc,
|
381
462
|
:reporter => issue.reporter }
|
382
463
|
|
383
|
-
|
384
|
-
f.puts data.to_yaml
|
385
|
-
f.close
|
386
|
-
editor = ENV["EDITOR"] || "/usr/bin/vi"
|
387
|
-
cmd = "#{editor} #{f.path.inspect}"
|
388
|
-
Ditz::debug "running: #{cmd}"
|
464
|
+
fn = run_editor { |f| f.puts data.to_yaml }
|
389
465
|
|
390
|
-
|
391
|
-
system cmd or raise Error, "cannot execute command: #{cmd.inspect}"
|
392
|
-
if File.mtime(f.path) == mtime
|
466
|
+
unless fn
|
393
467
|
puts "Aborted."
|
394
468
|
return
|
395
469
|
end
|
396
470
|
|
397
471
|
comment = ask_multiline "Comments"
|
472
|
+
|
398
473
|
begin
|
399
|
-
edits = YAML.load_file
|
474
|
+
edits = YAML.load_file fn
|
400
475
|
if issue.change edits, config.user, comment
|
401
476
|
puts "Changed recorded."
|
402
477
|
else
|
data/lib/release.rhtml
CHANGED
data/lib/unassigned.rhtml
CHANGED
data/lib/util.rb
CHANGED
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
|
|
3
3
|
specification_version: 1
|
4
4
|
name: ditz
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2008-04-
|
6
|
+
version: "0.2"
|
7
|
+
date: 2008-04-11 07:00:00 Z
|
8
8
|
summary: A simple issue tracker designed to integrate well with distributed version control systems like git and darcs. State is saved to a YAML file kept under version control, allowing issues to be closed/added/modified as part of a commit.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -32,7 +32,9 @@ files:
|
|
32
32
|
- Changelog
|
33
33
|
- README.txt
|
34
34
|
- Rakefile
|
35
|
+
- ReleaseNotes
|
35
36
|
- bin/ditz
|
37
|
+
- bin/ditz-convert-from-monolith
|
36
38
|
- lib/component.rhtml
|
37
39
|
- lib/ditz.rb
|
38
40
|
- lib/html.rb
|
@@ -56,6 +58,7 @@ extra_rdoc_files:
|
|
56
58
|
- README.txt
|
57
59
|
executables:
|
58
60
|
- ditz
|
61
|
+
- ditz-convert-from-monolith
|
59
62
|
extensions: []
|
60
63
|
|
61
64
|
requirements: []
|