ursm-ditz 0.4 → 0.5

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.
Files changed (50) hide show
  1. data/Changelog +15 -0
  2. data/INSTALL +20 -0
  3. data/LICENSE +674 -0
  4. data/Manifest.txt +40 -0
  5. data/PLUGINS.txt +140 -0
  6. data/README.txt +43 -27
  7. data/Rakefile +36 -3
  8. data/ReleaseNotes +6 -0
  9. data/bin/ditz +49 -63
  10. data/contrib/completion/ditz.bash +25 -9
  11. data/lib/ditz.rb +52 -7
  12. data/lib/ditz/file-storage.rb +54 -0
  13. data/lib/{hook.rb → ditz/hook.rb} +1 -1
  14. data/lib/{html.rb → ditz/html.rb} +42 -4
  15. data/lib/{lowline.rb → ditz/lowline.rb} +31 -11
  16. data/lib/{model-objects.rb → ditz/model-objects.rb} +53 -21
  17. data/lib/ditz/model.rb +321 -0
  18. data/lib/{operator.rb → ditz/operator.rb} +122 -67
  19. data/lib/ditz/plugins/git-sync.rb +83 -0
  20. data/lib/{plugins → ditz/plugins}/git.rb +57 -18
  21. data/lib/ditz/plugins/issue-claiming.rb +174 -0
  22. data/lib/ditz/plugins/issue-labeling.rb +161 -0
  23. data/lib/{util.rb → ditz/util.rb} +4 -0
  24. data/lib/{view.rb → ditz/view.rb} +0 -0
  25. data/lib/{views.rb → ditz/views.rb} +7 -4
  26. data/man/{ditz.1 → man1/ditz.1} +1 -1
  27. data/setup.rb +1585 -0
  28. data/share/ditz/blue-check.png +0 -0
  29. data/{lib → share/ditz}/component.rhtml +7 -5
  30. data/share/ditz/green-bar.png +0 -0
  31. data/share/ditz/green-check.png +0 -0
  32. data/share/ditz/index.rhtml +130 -0
  33. data/share/ditz/issue.rhtml +119 -0
  34. data/share/ditz/issue_table.rhtml +28 -0
  35. data/share/ditz/red-check.png +0 -0
  36. data/share/ditz/release.rhtml +98 -0
  37. data/share/ditz/style.css +226 -0
  38. data/share/ditz/unassigned.rhtml +23 -0
  39. data/share/ditz/yellow-bar.png +0 -0
  40. metadata +50 -28
  41. data/lib/index.rhtml +0 -113
  42. data/lib/issue.rhtml +0 -111
  43. data/lib/issue_table.rhtml +0 -33
  44. data/lib/model.rb +0 -208
  45. data/lib/plugins/issue-claiming.rb +0 -92
  46. data/lib/release.rhtml +0 -69
  47. data/lib/style.css +0 -127
  48. data/lib/trollop.rb +0 -518
  49. data/lib/unassigned.rhtml +0 -31
  50. data/lib/vendor/yaml_waml.rb +0 -28
@@ -6,16 +6,32 @@
6
6
 
7
7
  _ditz()
8
8
  {
9
- cur=${COMP_WORDS[COMP_CWORD]}
9
+ local cur=${COMP_WORDS[COMP_CWORD]}
10
+
10
11
  if [ $COMP_CWORD -eq 1 ]; then
11
- COMPREPLY=( $( compgen -W "$(ditz --commands)" $cur ) )
12
- elif [ $COMP_CWORD -eq 2 ]; then
13
- cmd=${COMP_WORDS[1]}
14
- COMPREPLY=( $( compgen -W "$(ditz "$cmd" '<options>' 2>/dev/null)" $cur ) )
15
- elif [ $COMP_CWORD -eq 3 ]; then
16
- cmd=${COMP_WORDS[1]}
17
- parm1=${COMP_WORDS[2]}
18
- COMPREPLY=( $( compgen -W "$(ditz "$cmd" "$parm1" '<options>' 2>/dev/null)" $cur ) )
12
+ # no command yet, show all commands
13
+ COMPREPLY=( $( compgen -W "$(ditz --commands)" -- $cur ) )
14
+
15
+ else
16
+ unset COMP_WORDS[COMP_CWORD] # remove last
17
+ unset COMP_WORDS[0] # remove first
18
+
19
+ # add options if applicable...
20
+ local options
21
+ if [ "${cur:0:1}" = '-' ]; then
22
+ # ...but only if at least a dash is given
23
+ case "${COMP_WORDS[1]}" in
24
+ add|add_reference|add_release|assign|close|comment|release|set_component|start|stop|unassign)
25
+ options="--comment --no-comment"
26
+ ;;
27
+ edit)
28
+ options="--comment --no-comment --silent"
29
+ ;;
30
+ esac
31
+ fi
32
+
33
+ # let ditz parse the commandline and print available completions, then append the options form above
34
+ COMPREPLY=( $( compgen -W "$(ditz "${COMP_WORDS[@]}" '<options>' 2>/dev/null) $options" -- $cur ) )
19
35
  fi
20
36
  }
21
37
 
@@ -1,9 +1,13 @@
1
+ require 'pathname'
2
+
1
3
  module Ditz
2
4
 
3
- VERSION = "0.4"
5
+ VERSION = "0.5"
6
+ attr_accessor :verbose
7
+ module_function :verbose, :verbose=
4
8
 
5
9
  def debug s
6
- puts "# #{s}" if $opts[:verbose]
10
+ puts "# #{s}" if $verbose || Ditz::verbose
7
11
  end
8
12
  module_function :debug
9
13
 
@@ -46,11 +50,52 @@ def find_ditz_file fn
46
50
  File.expand_path File.join(dir, fn)
47
51
  end
48
52
 
49
- module_function :home_dir, :find_dir_containing, :find_ditz_file
53
+ def load_plugins fn
54
+ Ditz::debug "loading plugins from #{fn}"
55
+ plugins = YAML::load_file fn
56
+ plugins.each do |p|
57
+ fn = Ditz::find_ditz_file "ditz/plugins/#{p}.rb"
58
+ Ditz::debug "loading plugin #{p.inspect} from #{fn}"
59
+ require File.expand_path(fn)
60
+ end
61
+ plugins
62
+ end
63
+
64
+ module_function :home_dir, :find_dir_containing, :find_ditz_file, :load_plugins
50
65
  end
51
66
 
52
- require 'model-objects'
53
- require 'operator'
54
- require 'views'
55
- require 'hook'
67
+ # Git-style automatic pagination of all output.
68
+ # Call run_pa ger from any opperator needing pagination.
69
+ # Yoinked from http://nex-3.com/posts/73-git-style-automatic-paging-in-ruby#comments
70
+ def run_pager
71
+ return if PLATFORM =~ /win32/
72
+ return unless STDOUT.tty?
73
+
74
+ read, write = IO.pipe
75
+
76
+ unless Kernel.fork # Child process
77
+ STDOUT.reopen(write)
78
+ STDERR.reopen(write) if STDERR.tty?
79
+ read.close
80
+ write.close
81
+ return
82
+ end
83
+
84
+ # Parent process, become pager
85
+ STDIN.reopen(read)
86
+ read.close
87
+ write.close
88
+
89
+ ENV['LESS'] ||= 'FSRX' # Don't page if the input is short enough, unless
90
+ # the user already have a LESS variable.
91
+
92
+ Kernel.select [STDIN] # Wait until we have input before we start the pager
93
+ pager = ENV['PAGER'] || 'less'
94
+ exec pager rescue exec "/bin/sh", "-c", pager
95
+ end
56
96
 
97
+ require 'ditz/model-objects'
98
+ require 'ditz/operator'
99
+ require 'ditz/views'
100
+ require 'ditz/hook'
101
+ require 'ditz/file-storage'
@@ -0,0 +1,54 @@
1
+ module Ditz
2
+
3
+ ## stores ditz database on disk
4
+ class FileStorage
5
+ PROJECT_FN = "project.yaml"
6
+ ISSUE_FN_GLOB = "issue-*.yaml"
7
+
8
+ def ISSUE_TO_FN i; "issue-#{i.id}.yaml" end
9
+
10
+ def initialize base_dir
11
+ @base_dir = base_dir
12
+ @project_fn = File.join @base_dir, PROJECT_FN
13
+ end
14
+
15
+ def load
16
+ Ditz::debug "loading project from #{@project_fn}"
17
+ project = Project.from @project_fn
18
+
19
+ fn = File.join @base_dir, ISSUE_FN_GLOB
20
+ Ditz::debug "loading issues from #{fn}"
21
+ project.issues = Dir[fn].map { |fn| Issue.from fn }
22
+ Ditz::debug "found #{project.issues.size} issues"
23
+
24
+ project.issues.each { |i| i.project = project }
25
+ project
26
+ end
27
+
28
+ def save project
29
+ dirty = false
30
+ dirty = project.each_modelobject { |o| break true if o.changed? }
31
+ if dirty
32
+ Ditz::debug "project is dirty, saving #{@project_fn}"
33
+ project.save! @project_fn
34
+ end
35
+
36
+ changed_issues = project.issues.select { |i| i.changed? }
37
+ changed_issues.each do |i|
38
+ fn = filename_for_issue i
39
+ Ditz::debug "issue #{i.name} is dirty, saving #{fn}"
40
+ i.save! fn
41
+ end
42
+
43
+ project.deleted_issues.each do |i|
44
+ fn = filename_for_issue i
45
+ Ditz::debug "issue #{i.name} has been deleted, deleting #{fn}"
46
+ FileUtils.rm fn
47
+ end
48
+ end
49
+
50
+ def filename_for_issue i; File.join @base_dir, ISSUE_TO_FN(i) end
51
+ def filename_for_project; @project_fn end
52
+ end
53
+
54
+ end
@@ -57,7 +57,7 @@ EOS
57
57
  files = dirs.map { |d| Dir[File.join(d, "*.rb")] }.flatten
58
58
  files.each do |fn|
59
59
  Ditz::debug "loading hook file #{fn}"
60
- load fn
60
+ require File.expand_path(fn)
61
61
  end
62
62
  end
63
63
 
@@ -48,16 +48,54 @@ class ErbHtml
48
48
  raise ArgumentError, "no link for #{o.inspect}" unless dest
49
49
  "<a href=\"#{dest}\">#{name}</a>"
50
50
  end
51
- def fancy_issue_link_for i
52
- "<span class=\"issuestatus_#{i.status}\">" + link_to(i, "[#{i.title}]") + "</span>"
51
+
52
+ def issue_status_img_for i, opts={}
53
+ fn, title = if i.closed?
54
+ case i.disposition
55
+ when :fixed; ["green-check.png", "fixed"]
56
+ when :wontfix; ["red-check.png", "won't fix"]
57
+ when :reorg; ["blue-check.png", "reorganized"]
58
+ end
59
+ elsif i.in_progress?
60
+ ["green-bar.png", "in progress"]
61
+ elsif i.paused?
62
+ ["yellow-bar.png", "paused"]
63
+ end
64
+
65
+ return "" unless fn
66
+
67
+ args = {:src => fn, :alt => title, :title => title}
68
+ args[:class] = opts[:class] if opts[:class]
69
+
70
+ "<img " + args.map { |k, v| "#{k}=#{v.inspect}" }.join(" ") + "/>"
53
71
  end
54
72
 
55
- def link_issue_names project, s
73
+ def issue_link_for i, opts={}
74
+ link = if opts[:inline]
75
+ "<span class=\"inline-issue-link\">" + link_to(i, "issue <span class=\"id\">#{i.id[0,8]}</span>: #{i.title}") + "</span>"
76
+ else
77
+ link_to i, i.title
78
+ end
79
+ link = link + " " + issue_status_img_for(i, :class => "inline-status-image") if opts[:status_image]
80
+ link
81
+ end
82
+
83
+ def link_issue_names project, s, opts={}
56
84
  project.issues.inject(s) do |s, i|
57
- s.gsub(/\b#{i.name}\b/, fancy_issue_link_for(i))
85
+ s.gsub(/\b#{i.name}\b/, issue_link_for(i, {:inline => true, :status_image => true}.merge(opts)))
58
86
  end
59
87
  end
60
88
 
89
+ def progress_meter p, size=50
90
+ done = (p * size).to_i
91
+ undone = [size - done, 0].max
92
+ "<span class='progress-meter'><span class='progress-meter-done'>" +
93
+ ("&nbsp;" * done) +
94
+ "</span><span class='progress-meter-undone'>" +
95
+ ("&nbsp;" * undone) +
96
+ "</span></span>"
97
+ end
98
+
61
99
  ## render a nested ERB
62
100
  alias :render :render_template
63
101
 
@@ -1,5 +1,5 @@
1
1
  require 'tempfile'
2
- require "util"
2
+ require "ditz/util"
3
3
 
4
4
  class Numeric
5
5
  def to_pretty_s
@@ -57,7 +57,9 @@ module Lowline
57
57
  yield f
58
58
  f.close
59
59
 
60
- editor = ENV["EDITOR"] || "/usr/bin/vi"
60
+ editor = ENV["EDITOR"]
61
+ editor ||= "/usr/bin/sensible-editor" if File.exist?("/usr/bin/sensible-editor")
62
+ editor ||= "/usr/bin/vi"
61
63
  cmd = "#{editor} #{f.path.inspect}"
62
64
 
63
65
  mtime = File.mtime f.path
@@ -98,11 +100,10 @@ module Lowline
98
100
 
99
101
  def ask_via_editor q, default=nil
100
102
  fn = run_editor do |f|
103
+ f.puts(default || "")
101
104
  f.puts q.gsub(/^/, "## ")
102
105
  f.puts "##"
103
- f.puts "## Enter your text below. Lines starting with a '#' will be ignored."
104
- f.puts
105
- f.puts default if default
106
+ f.puts "## Enter your text above. Lines starting with a '#' will be ignored."
106
107
  end
107
108
  return unless fn
108
109
  IO.read(fn).gsub(/^#.*$/, "").multistrip
@@ -139,6 +140,15 @@ module Lowline
139
140
  ans.multistrip
140
141
  end
141
142
 
143
+ def can_run_editor?
144
+ !ENV["EDITOR"].nil? || File.exist?("/usr/bin/sensible-editor") || File.exist?("/usr/bin/vi")
145
+ end
146
+
147
+ def ask_multiline_smartly q
148
+ can_run_editor? ? ask_via_editor(q) : ask_multiline(q)
149
+ end
150
+
151
+
142
152
  def ask_yon q
143
153
  while true
144
154
  print "#{q} (y/n): "
@@ -175,8 +185,15 @@ module Lowline
175
185
  stuff
176
186
  end
177
187
 
178
- def ask_for_selection stuff, name, to_string=:to_s
179
- puts "Choose a #{name}:"
188
+ def ask_for_selection stuff, name, to_string=:to_s, many=false
189
+ if many
190
+ return [] if stuff.empty?
191
+ name = name.pluralize(2, false)
192
+ puts "Choose one or more #{name} (comma separated list):"
193
+ else
194
+ return nil if stuff.empty?
195
+ puts "Choose a #{name}:"
196
+ end
180
197
  stuff.each_with_index do |c, i|
181
198
  pretty = case to_string
182
199
  when block_given? && to_string # heh
@@ -191,12 +208,15 @@ module Lowline
191
208
  puts " #{i + 1}) #{pretty}"
192
209
  end
193
210
 
194
- j = while true
195
- i = ask "#{name.capitalize} (1--#{stuff.size})"
196
- break i.to_i if i && (1 .. stuff.size).member?(i.to_i)
211
+ js = while true
212
+ is = ask "#{name.capitalize} (1--#{stuff.size})"
213
+ next unless is
214
+ is = is.strip.split(/\s*,\s*/).map { |i| i.to_i }
215
+ break is if is.all? { |i| (1 .. stuff.size).member?(i) }
197
216
  end
198
217
 
199
- stuff[j - 1]
218
+ ss = js.map { |j| stuff[j - 1] }
219
+ (many)? ss : ss.first
200
220
  end
201
221
  end
202
222
 
@@ -1,4 +1,4 @@
1
- require 'model'
1
+ require 'ditz/model'
2
2
 
3
3
  module Ditz
4
4
 
@@ -36,17 +36,37 @@ end
36
36
  class Project < ModelObject
37
37
  class Error < StandardError; end
38
38
 
39
- attr_accessor :pathname
40
-
41
- field :name, :default_generator => lambda { File.basename(Dir.pwd) }
39
+ field :name, :prompt => "Project name", :default_generator => lambda { File.basename(Dir.pwd) }
42
40
  field :version, :default => Ditz::VERSION, :ask => false
43
- field :components, :multi => true, :generator => :get_components
41
+ field :components, :multi => true, :interactive_generator => :get_components
44
42
  field :releases, :multi => true, :ask => false
45
43
 
44
+ attr_accessor :pathname
45
+
46
46
  ## issues are not model fields proper, so we build up their interface here.
47
- attr_accessor :issues
48
- def add_issue issue; added_issues << issue; issues << issue end
49
- def drop_issue issue; deleted_issues << issue if issues.delete issue end
47
+ attr_reader :issues
48
+ def issues= issues
49
+ @issues = issues
50
+ @issues.each { |i| i.project = self }
51
+ assign_issue_names!
52
+ issues
53
+ end
54
+
55
+ def add_issue issue
56
+ added_issues << issue
57
+ issues << issue
58
+ issue.project = self
59
+ assign_issue_names!
60
+ issue
61
+ end
62
+
63
+ def drop_issue issue
64
+ if issues.delete issue
65
+ deleted_issues << issue
66
+ assign_issue_names!
67
+ end
68
+ end
69
+
50
70
  def added_issues; @added_issues ||= [] end
51
71
  def deleted_issues; @deleted_issues ||= [] end
52
72
 
@@ -63,7 +83,7 @@ EOS
63
83
 
64
84
  def issues_for ident
65
85
  by_name = issues.find { |i| i.name == ident }
66
- by_name ? [by_name] : issues.select { |i| i.id =~ /^#{ident}/ }
86
+ by_name ? [by_name] : issues.select { |i| i.id =~ /^#{Regexp::escape ident}/ }
67
87
  end
68
88
 
69
89
  def component_for component_name
@@ -107,6 +127,12 @@ EOS
107
127
  raise Error, "more than one release named #{dup.inspect}"
108
128
  end
109
129
  end
130
+
131
+ def self.from *a
132
+ p = super(*a)
133
+ p.validate!
134
+ p
135
+ end
110
136
  end
111
137
 
112
138
  class Issue < ModelObject
@@ -114,9 +140,9 @@ class Issue < ModelObject
114
140
 
115
141
  field :title
116
142
  field :desc, :prompt => "Description", :multiline => true
117
- field :type, :generator => :get_type
118
- field :component, :generator => :get_component
119
- field :release, :generator => :get_release
143
+ field :type, :interactive_generator => :get_type
144
+ field :component, :interactive_generator => :get_component
145
+ field :release, :interactive_generator => :get_release, :nil_ok => true
120
146
  field :reporter, :prompt => "Issue creator", :default_generator => lambda { |config, proj| config.user }
121
147
  field :status, :ask => false, :default => :unstarted
122
148
  field :disposition, :ask => false
@@ -194,6 +220,7 @@ class Issue < ModelObject
194
220
  def feature?; type == :feature end
195
221
  def unassigned?; release.nil? end
196
222
  def assigned?; !unassigned? end
223
+ def paused?; status == :paused end
197
224
 
198
225
  def start_work who, comment; change_status :in_progress, who, comment end
199
226
  def stop_work who, comment
@@ -216,27 +243,29 @@ class Issue < ModelObject
216
243
  end
217
244
  private :change_status
218
245
 
219
- def change hash, who, comment
246
+ def change hash, who, comment, silent
220
247
  what = []
221
248
  if title != hash[:title]
222
- what << "changed title"
249
+ what << "title"
223
250
  self.title = hash[:title]
224
251
  end
225
252
 
226
253
  if desc != hash[:description]
227
- what << "changed description"
254
+ what << "description"
228
255
  self.desc = hash[:description]
229
256
  end
230
257
 
231
258
  if reporter != hash[:reporter]
232
- what << "changed reporter"
259
+ what << "reporter"
233
260
  self.reporter = hash[:reporter]
234
261
  end
235
262
 
236
- unless what.empty?
237
- log what.join(", "), who, comment
263
+ unless what.empty? || silent
264
+ log "edited " + what.join(", "), who, comment
238
265
  true
239
266
  end
267
+
268
+ !what.empty?
240
269
  end
241
270
 
242
271
  def assign_to_release release, who, comment
@@ -289,15 +318,18 @@ end
289
318
  class Config < ModelObject
290
319
  field :name, :prompt => "Your name", :default_generator => :get_default_name
291
320
  field :email, :prompt => "Your email address", :default_generator => :get_default_email
292
- field :issue_dir, :ask => false, :default => "bugs"
321
+ field :issue_dir, :prompt => "Directory to store issues state in", :default => ".ditz"
293
322
 
294
323
  def user; "#{name} <#{email}>" end
295
324
 
296
325
  def get_default_name
297
326
  require 'etc'
298
327
 
299
- name = Etc.getpwnam(ENV["USER"])
300
- name = name ? name.gecos.split(/,/).first : ""
328
+ name = if ENV["USER"]
329
+ pwent = Etc.getpwnam ENV["USER"]
330
+ pwent ? pwent.gecos.split(/,/).first : nil
331
+ end
332
+ name || "Ditz User"
301
333
  end
302
334
 
303
335
  def get_default_email