ditz 0.1.2 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|