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.
- data/Changelog +15 -0
- data/INSTALL +20 -0
- data/LICENSE +674 -0
- data/Manifest.txt +40 -0
- data/PLUGINS.txt +140 -0
- data/README.txt +43 -27
- data/Rakefile +36 -3
- data/ReleaseNotes +6 -0
- data/bin/ditz +49 -63
- data/contrib/completion/ditz.bash +25 -9
- data/lib/ditz.rb +52 -7
- data/lib/ditz/file-storage.rb +54 -0
- data/lib/{hook.rb → ditz/hook.rb} +1 -1
- data/lib/{html.rb → ditz/html.rb} +42 -4
- data/lib/{lowline.rb → ditz/lowline.rb} +31 -11
- data/lib/{model-objects.rb → ditz/model-objects.rb} +53 -21
- data/lib/ditz/model.rb +321 -0
- data/lib/{operator.rb → ditz/operator.rb} +122 -67
- data/lib/ditz/plugins/git-sync.rb +83 -0
- data/lib/{plugins → ditz/plugins}/git.rb +57 -18
- data/lib/ditz/plugins/issue-claiming.rb +174 -0
- data/lib/ditz/plugins/issue-labeling.rb +161 -0
- data/lib/{util.rb → ditz/util.rb} +4 -0
- data/lib/{view.rb → ditz/view.rb} +0 -0
- data/lib/{views.rb → ditz/views.rb} +7 -4
- data/man/{ditz.1 → man1/ditz.1} +1 -1
- data/setup.rb +1585 -0
- data/share/ditz/blue-check.png +0 -0
- data/{lib → share/ditz}/component.rhtml +7 -5
- data/share/ditz/green-bar.png +0 -0
- data/share/ditz/green-check.png +0 -0
- data/share/ditz/index.rhtml +130 -0
- data/share/ditz/issue.rhtml +119 -0
- data/share/ditz/issue_table.rhtml +28 -0
- data/share/ditz/red-check.png +0 -0
- data/share/ditz/release.rhtml +98 -0
- data/share/ditz/style.css +226 -0
- data/share/ditz/unassigned.rhtml +23 -0
- data/share/ditz/yellow-bar.png +0 -0
- metadata +50 -28
- data/lib/index.rhtml +0 -113
- data/lib/issue.rhtml +0 -111
- data/lib/issue_table.rhtml +0 -33
- data/lib/model.rb +0 -208
- data/lib/plugins/issue-claiming.rb +0 -92
- data/lib/release.rhtml +0 -69
- data/lib/style.css +0 -127
- data/lib/trollop.rb +0 -518
- data/lib/unassigned.rhtml +0 -31
- 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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
|
data/lib/ditz.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
1
3
|
module Ditz
|
2
4
|
|
3
|
-
VERSION = "0.
|
5
|
+
VERSION = "0.5"
|
6
|
+
attr_accessor :verbose
|
7
|
+
module_function :verbose, :verbose=
|
4
8
|
|
5
9
|
def debug s
|
6
|
-
puts "# #{s}" if $
|
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
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
@@ -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
|
-
|
52
|
-
|
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
|
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/,
|
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
|
+
(" " * done) +
|
94
|
+
"</span><span class='progress-meter-undone'>" +
|
95
|
+
(" " * 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"]
|
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
|
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
|
-
|
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
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
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, :
|
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
|
-
|
48
|
-
def
|
49
|
-
|
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, :
|
118
|
-
field :component, :
|
119
|
-
field :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 << "
|
249
|
+
what << "title"
|
223
250
|
self.title = hash[:title]
|
224
251
|
end
|
225
252
|
|
226
253
|
if desc != hash[:description]
|
227
|
-
what << "
|
254
|
+
what << "description"
|
228
255
|
self.desc = hash[:description]
|
229
256
|
end
|
230
257
|
|
231
258
|
if reporter != hash[:reporter]
|
232
|
-
what << "
|
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, :
|
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 =
|
300
|
-
|
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
|