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 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 "scp -Cr ditz wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
45
+ sh "rsync -essh -cavz ditz wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
46
46
  end
47
47
 
48
48
  # vim: syntax=ruby
@@ -0,0 +1,8 @@
1
+ 0.2
2
+ ---
3
+
4
+ In ditz 0.2, we store issues per file. This avoids many unnecessary conflicts
5
+ that occur in the single-file case.
6
+
7
+ To upgrade your bugs.yaml to a bugs/ directory, you must run
8
+ ditz-convert-from-monolith.
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} (c) 2008 William Morgan"
9
- opt :issue_file, "Issue database file", :default => "bugs.yaml"
10
- opt :config_file, "Configuration file", :default => File.join(ENV["HOME"], ".ditz-config")
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: init and help
23
+ case cmd # some special cases not handled by Ditz::Operator
18
24
  when "init"
19
- fn = $opts[:issue_file]
20
- die "#{fn} already exists" if File.exists? fn
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, #{fn} created successfully."
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
- Ditz::debug "loading issues from #{$opts[:issue_file]}"
37
+ die "No #{dir} directory---use 'ditz init' to initialize" unless File.exists? dir
38
+
31
39
  project = begin
32
- Ditz::Project.from $opts[:issue_file]
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
- fn = ".ditz-config"
43
- if File.exists? fn
44
- Ditz::debug "loading config from #{fn}"
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
- op.do cmd, project, config, *args
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
- Ditz::debug "project is dirty, saving"
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! $opts[:issue_file]
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."
@@ -13,6 +13,6 @@
13
13
 
14
14
  <%= render "issue_table", :show_component => false, :show_release => true %>
15
15
 
16
- <p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
16
+ <p class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.
17
17
  </body>
18
18
  </html>
@@ -1,6 +1,6 @@
1
1
  module Ditz
2
2
 
3
- VERSION = "0.1.2"
3
+ VERSION = "0.2"
4
4
 
5
5
  def debug s
6
6
  puts "# #{s}" if $opts[:verbose]
@@ -77,7 +77,7 @@
77
77
  </ul>
78
78
  <% end %>
79
79
 
80
- <p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
80
+ <p class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.
81
81
 
82
82
  </body>
83
83
  </html>
@@ -101,6 +101,6 @@
101
101
  <% end %>
102
102
  </table>
103
103
 
104
- <p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
104
+ <p class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.
105
105
  </body>
106
106
  </html>
@@ -1,15 +1,16 @@
1
- require 'util'
1
+ require 'tempfile'
2
+ require "util"
2
3
 
3
4
  class Numeric
4
5
  def to_pretty_s
5
- if self < 20
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 . by itself to stop):"
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
- break if line =~ /^\.$/ || line.nil?
87
- ans << line.strip + "\n"
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.sub(/\n+$/, "")
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
@@ -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 } or
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 } or
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 } or
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
@@ -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
- Ditz::debug "saving configuration to #{fn}"
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 initialize
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={}
@@ -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
- Registered commands:
102
+ Ditz commands:
103
+
40
104
  EOS
41
105
  ops = self.class.operations
42
- len = ops.map { |name, desc| name.to_s.length }.max
43
- ops.each { |name, desc| printf "%#{len}s: %s\n", name, desc }
44
- puts
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, issue_name
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, issue_name
82
- issue = project.issue_for issue_name
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, release=nil
124
- if project.releases.empty?
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 bugs.empty? && feats.empty?
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, release=nil
166
- actually_do_todo project, config, release, false
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, release=nil
171
- actually_do_todo project, config, release, true
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, release, full
175
- parse_releases_arg(project, release).each do |r, bugs, feats|
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
- print todo_list_for(issues.sort_by { |i| i.sort_order })
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, name
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, name
221
- issue = project.issue_for name
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, name
229
- issue = project.issue_for name
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, name
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, issue_name
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
- release = ask_for_selection project.releases, "release", :name
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, issue_name
261
- issue = project.issue_for issue_name
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, issue_name
269
- issue = project.issue_for issue_name
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.each do |r|
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, release_name
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, relnames
293
- parse_releases_arg(project, relnames).each do |r, bugs, feats|
294
- puts "== #{r.name} / #{r.release_time.pretty_date}" if r.released?
295
- feats.select { |f| f.closed? }.each { |i| puts "* #{i.title}" }
296
- bugs.select { |f| f.closed? }.each { |i| puts "* bugfix: #{i.title}" }
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="html"
302
- #FileUtils.rm_rf dir
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
- print_todo issues
439
+ puts(todo_list_for(issues) || "No matching issues.")
375
440
  end
376
441
 
377
- operation :edit, "Edit an issue"
378
- def edit project, config, issue_name
379
- issue = project.issue_for issue_name
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
- f = Tempfile.new("ditz")
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
- mtime = File.mtime f.path
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 f.path
474
+ edits = YAML.load_file fn
400
475
  if issue.change edits, config.user, comment
401
476
  puts "Changed recorded."
402
477
  else
@@ -61,7 +61,7 @@
61
61
  <% end %>
62
62
  </table>
63
63
 
64
- <p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
64
+ <p class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.
65
65
 
66
66
  </body>
67
67
  </html>
@@ -22,6 +22,6 @@
22
22
  <% end %>
23
23
  </table>
24
24
 
25
- <p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
25
+ <p class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.
26
26
  </body>
27
27
  </html>
@@ -30,4 +30,15 @@ class Array
30
30
  def to_h
31
31
  Hash[*flatten]
32
32
  end
33
+
34
+ def flatten_one_level
35
+ inject([]) do |ret, e|
36
+ case e
37
+ when Array
38
+ ret + e
39
+ else
40
+ ret << e
41
+ end
42
+ end
43
+ end
33
44
  end
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.1.2
7
- date: 2008-04-04 00:00:00 -07:00
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: []