ursm-ditz 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,83 @@
1
+ ## git-sync ditz plugin
2
+ ##
3
+ ## This plugin is useful for when you want synchronized, non-distributed issue
4
+ ## coordination with other developers, and you're using git. It allows you to
5
+ ## synchronize issue updates with other developers by using the 'ditz sync'
6
+ ## command, which does all the git work of sending and receiving issue change
7
+ ## for you. However, you have to set things up in a very specific way for this
8
+ ## to work:
9
+ ##
10
+ ## 1. Your ditz state must be on a separate branch. I recommend calling it
11
+ ## 'bugs'. Create this branch, do a ditz init, and push it to the remote
12
+ ## repo. (This means you won't be able to mingle issue change and code
13
+ ## change in the same commits. If you care.)
14
+ ## 2. Make a checkout of the bugs branch in a separate directory, but NOT in
15
+ ## your code checkout. If you're developing in a directory called "project",
16
+ ## I recommend making a ../project-bugs/ directory, cloning the repo there
17
+ ## as well, and keeping that directory checked out to the 'bugs' branch.
18
+ ## (There are various complicated things you can do to make that directory
19
+ ## share git objects with your code directory, but I wouldn't bother unless
20
+ ## you really care about disk space. Just make it an independent clone.)
21
+ ## 3. Set that directory as your issue-dir in your .ditz-config file in your
22
+ ## code checkout directory. (This file should be in .gitignore, btw.)
23
+ ## 4. Run 'ditz reconfigure' and fill in the local branch name, remote
24
+ ## branch name, and remote repo for the issue tracking branch.
25
+ ##
26
+ ## Once that's set up, 'ditz sync' will change to the bugs checkout dir, bundle
27
+ ## up any changes you've made to issue status, push them to the remote repo,
28
+ ## and pull any new changes in too. All ditz commands will read from your bugs
29
+ ## directory, so you should be able to use ditz without caring about where
30
+ ## things are anymore.
31
+ ##
32
+ ## This complicated setup is necessary to avoid accidentally mingling code
33
+ ## change and issue change. With this setup, issue change is synchronized,
34
+ ## but how you synchronize code is still up to you.
35
+ ##
36
+ ## Usage:
37
+ ## 0. read all the above text very carefully
38
+ ## 1. add a line "- git-sync" to the .ditz-plugins file in the project
39
+ ## root
40
+ ## 2. run 'ditz reconfigure' and answer its questions
41
+ ## 3. run 'ditz sync' with abandon
42
+
43
+ module Ditz
44
+
45
+ class Config
46
+ field :git_sync_local_branch, :prompt => "Local bugs branch name for ditz sync", :default => "bugs"
47
+ field :git_sync_remote_branch, :prompt => "Remote bugs branch name for ditz sync", :default => "bugs"
48
+ field :git_sync_remote_repo, :prompt => "Remote bugs repo name for ditz sync", :default => "origin"
49
+ end
50
+
51
+ class Operator
52
+ operation :sync, "Sync the repo containing ditz via git" do
53
+ opt :dry_run, "Dry run: print the commants, but don't execute them", :short => "n"
54
+ end
55
+ def sync project, config, opts
56
+ unless config.git_sync_local_branch
57
+ $stderr.puts "Please run ditz reconfigure and set the local and remote branch names"
58
+ return
59
+ end
60
+
61
+ Dir.chdir $project_root
62
+ puts "[in #{$project_root}]"
63
+ sh "git add *.yaml", :force => true, :fake => opts[:dry_run]
64
+ sh "git commit -m 'issue updates'", :force => true, :fake => opts[:dry_run]
65
+ sh "git pull", :fake => opts[:dry_run]
66
+ sh "git push #{config.git_sync_remote_repo} #{config.git_sync_local_branch}:#{config.git_sync_remote_branch}", :fake => opts[:dry_run]
67
+ puts
68
+ puts "Ditz issue state synchronized."
69
+ end
70
+
71
+ private
72
+
73
+ def sh s, opts={}
74
+ puts "+ #{s}"
75
+ return if opts[:fake]
76
+ unless system(s) || opts[:force]
77
+ $stderr.puts "non-zero (#{$?.exitstatus}) exit code: #{s}"
78
+ exit(-1)
79
+ end
80
+ end
81
+ end
82
+
83
+ end
@@ -1,3 +1,31 @@
1
+ ## git ditz plugin
2
+ ##
3
+ ## This plugin allows issues to be associated with git commits and git
4
+ ## branches. Git commits can be easily tagged with a ditz issue with the 'ditz
5
+ ## commit' command, and both 'ditz show' and the ditz HTML output will then
6
+ ## contain a list of associated commits for each issue.
7
+ ##
8
+ ## Issues can also be assigned a single git feature branch. In this case, all
9
+ ## commits on that branch will listed as commits for that issue. This
10
+ ## particular feature is fairly rudimentary, however---it assumes the reference
11
+ ## point is the 'master' branch, and once the feature branch is merged back
12
+ ## into master, the list of commits disappears.
13
+ ##
14
+ ## Two configuration variables are added, which, when specified, are used to
15
+ ## construct HTML links for the git commit id and branch names in the generated
16
+ ## HTML output.
17
+ ##
18
+ ## Commands added:
19
+ ## ditz set-branch: set the git branch of an issue
20
+ ## ditz commit: run git-commit, and insert the issue id into the commit
21
+ ## message.
22
+ ##
23
+ ## Usage:
24
+ ## 1. add a line "- git" to the .ditz-plugins file in the project root
25
+ ## 2. run ditz reconfigure, and enter the URL prefixes, if any, from
26
+ ## which to create commit and branch links.
27
+ ## 3. use 'ditz commit' with abandon.
28
+
1
29
  require 'time'
2
30
 
3
31
  module Ditz
@@ -11,7 +39,7 @@ class Issue
11
39
  filters << "master..#{git_branch}" if git_branch
12
40
 
13
41
  output = filters.map do |f|
14
- `git log --pretty=format:\"%aD\t%an <%ae>\t%h\t%s\" #{f}`
42
+ `git log --pretty=format:\"%aD\t%an <%ae>\t%h\t%s\" #{f} 2> /dev/null`
15
43
  end.join
16
44
 
17
45
  @git_commits = output.split(/\n/).map { |l| l.split("\t") }.
@@ -55,17 +83,14 @@ EOS
55
83
 
56
84
  [<<EOS, { :commits => commits, :url_prefix => config.git_commit_url_prefix }]
57
85
  <h2>Commits for this issue</h2>
58
- <table>
86
+ <table class="log">
59
87
  <% commits.each_with_index do |(time, who, hash, msg), i| %>
60
- <% if i % 2 == 0 %>
61
- <tr class="logentryeven">
62
- <% else %>
63
- <tr class="logentryodd">
64
- <% end %>
65
- <td class="logtime"><%=t time %></td>
66
- <td class="logwho"><%=obscured_email who %></td>
67
- <td class="logwhat"><%=h msg %> [<%= url_prefix && !url_prefix.blank? ? link_to([url_prefix, hash].join, hash) : hash %>]</td>
88
+ <tr class="<%= i % 2 == 0 ? "even-row" : "odd-row" %>">
89
+ <td class="time"><%=t time %></td>
90
+ <td class="person"><%=obscured_email who %></td>
91
+ <td class="message"><%=h msg %> [<%= url_prefix && !url_prefix.blank? ? link_to([url_prefix, hash].join, hash) : hash %>]</td>
68
92
  </tr>
93
+ <tr><td></td></tr>
69
94
  <% end %>
70
95
  </table>
71
96
  EOS
@@ -98,16 +123,30 @@ class Operator
98
123
  :short => "-v", :default => false
99
124
  opt :message, "Use the given <s> as the commit message.", \
100
125
  :short => "-m", :type => :string
126
+ opt :edit, "Further edit the message, even if --message is given.", :short => "-e", :default => false
101
127
  end
128
+
102
129
  def commit project, config, opts, issue
103
- verbose_flag = opts[:verbose] ? "--verbose" : ""
104
- all_flag = opts[:all] ? "--all" : ""
105
- ditz_header = "Ditz-issue: #{issue.id}"
106
- message = opts[:message] ? "#{opts[:message]}\n\n#{ditz_header}" : \
107
- "#{ditz_header}"
108
- edit_flag = opts[:message] ? "" : "--edit"
109
- message_flag = %{--message="#{message}"}
110
- exec "git commit #{all_flag} #{verbose_flag} #{message_flag} #{edit_flag}"
130
+ opts[:edit] = true if opts[:message].nil?
131
+
132
+ args = {
133
+ :verbose => "--verbose",
134
+ :all => "--all",
135
+ :edit => "--edit",
136
+ }.map { |k, v| opts[k] ? v : "" }.join(" ")
137
+
138
+ comment = "# #{issue.name}: #{issue.title}"
139
+ tag = "Ditz-issue: #{issue.id}"
140
+ message = if opts[:message] && !opts[:edit]
141
+ "#{opts[:message]}\n\n#{tag}"
142
+ elsif opts[:message] && opts[:edit]
143
+ "#{opts[:message]}\n\n#{comment}\n#{tag}"
144
+ else
145
+ "#{comment}\n#{tag}"
146
+ end
147
+
148
+ message = message.gsub("\"", "\\\"")
149
+ exec "git commit #{args} --message=\"#{message}\""
111
150
  end
112
151
  end
113
152
 
@@ -0,0 +1,174 @@
1
+ ## issue-claiming ditz plugin
2
+ ##
3
+ ## This plugin allows people to claim issues. This is useful for avoiding
4
+ ## duplication of work---you can check to see if someone's claimed an
5
+ ## issue before starting to work on it, and you can let people know what
6
+ ## you're working on.
7
+ ##
8
+ ## Commands added:
9
+ ## ditz claim: claim an issue for yourself
10
+ ## ditz unclaim: unclaim a claimed issue
11
+ ## ditz mine: show all issues claimed by you
12
+ ## ditz claimed: show all claimed issues, by developer
13
+ ## ditz unclaimed: show all unclaimed issues
14
+ ##
15
+ ## Usage:
16
+ ## 1. add a line "- issue-claiming" to the .ditz-plugins file in the project
17
+ ## root
18
+ ## 2. use the above commands to abandon
19
+
20
+ module Ditz
21
+ class Issue
22
+ field :claimer, :ask => false
23
+
24
+ def claim who, comment, force=false
25
+ raise Error, "already claimed by #{claimer}" if claimer && !force
26
+ log "issue claimed", who, comment
27
+ self.claimer = who
28
+ end
29
+
30
+ def unclaim who, comment, force=false
31
+ raise Error, "not claimed" unless claimer
32
+ raise Error, "can only be unclaimed by #{claimer}" unless claimer == who || force
33
+ if claimer == who
34
+ log "issue unclaimed", who, comment
35
+ else
36
+ log "unassigned from #{claimer}", who, comment
37
+ end
38
+ self.claimer = nil
39
+ end
40
+
41
+ def claimed?; claimer end
42
+ def unclaimed?; !claimed? end
43
+ end
44
+
45
+ class ScreenView
46
+ add_to_view :issue_summary do |issue, config|
47
+ " Claimed by: #{issue.claimer || 'none'}\n"
48
+ end
49
+ end
50
+
51
+ class HtmlView
52
+ add_to_view :issue_summary do |issue, config|
53
+ next unless issue.claimer
54
+ [<<EOS, { :issue => issue }]
55
+ <tr>
56
+ <td class='attrname'>Claimed by:</td>
57
+ <td class='attrval'><%= h(issue.claimer) %></td>
58
+ </tr>
59
+ EOS
60
+ end
61
+ end
62
+
63
+ class Operator
64
+ alias :__issue_claiming_start :start
65
+ def start project, config, opts, issue
66
+ if issue.claimed? && issue.claimer != config.user
67
+ raise Error, "issue #{issue.name} claimed by #{issue.claimer}"
68
+ else
69
+ __issue_claiming_start project, config, opts, issue
70
+ end
71
+ end
72
+
73
+ alias :__issue_claiming_stop :stop
74
+ def stop project, config, opts, issue
75
+ if issue.claimed? && issue.claimer != config.user
76
+ raise Error, "issue #{issue.name} claimed by #{issue.claimer}"
77
+ else
78
+ __issue_claiming_stop project, config, opts, issue
79
+ end
80
+ end
81
+
82
+ alias :__issue_claiming_close :close
83
+ def close project, config, opts, issue
84
+ if issue.claimed? && issue.claimer != config.user
85
+ raise Error, "issue #{issue.name} claimed by #{issue.claimer}"
86
+ else
87
+ __issue_claiming_close project, config, opts, issue
88
+ end
89
+ end
90
+
91
+ operation :claim, "Claim an issue for yourself", :issue do
92
+ opt :force, "Claim this issue even if someone else has claimed it", :default => false
93
+ end
94
+ def claim project, config, opts, issue
95
+ puts "Claiming issue #{issue.name}: #{issue.title}."
96
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
97
+ issue.claim config.user, comment, opts[:force]
98
+ puts "Issue #{issue.name} marked as claimed by #{config.user}"
99
+ end
100
+
101
+ operation :unclaim, "Unclaim a claimed issue", :issue do
102
+ opt :force, "Unclaim this issue even if it's claimed by someone else", :default => false
103
+ end
104
+ def unclaim project, config, opts, issue
105
+ puts "Unclaiming issue #{issue.name}: #{issue.title}."
106
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
107
+ issue.unclaim config.user, comment, opts[:force]
108
+ puts "Issue #{issue.name} marked as unclaimed."
109
+ end
110
+
111
+ operation :mine, "Show all issues claimed by you", :maybe_release do
112
+ opt :all, "Show all issues, not just open ones"
113
+ end
114
+ def mine project, config, opts, releases
115
+ releases ||= project.unreleased_releases + [:unassigned]
116
+ releases = [*releases]
117
+
118
+ issues = project.issues.select do |i|
119
+ r = project.release_for(i.release) || :unassigned
120
+ releases.member?(r) && i.claimer == config.user && (opts[:all] || i.open?)
121
+ end
122
+ if issues.empty?
123
+ puts "No issues."
124
+ else
125
+ print_todo_list_by_release_for project, issues
126
+ end
127
+ end
128
+
129
+ operation :claimed, "Show claimed issues by claimer", :maybe_release do
130
+ opt :all, "Show all issues, not just open ones"
131
+ end
132
+ def claimed project, config, opts, releases
133
+ releases ||= project.unreleased_releases + [:unassigned]
134
+ releases = [*releases]
135
+
136
+ issues = project.issues.inject({}) do |h, i|
137
+ r = project.release_for(i.release) || :unassigned
138
+ if i.claimed? && (opts[:all] || i.open?) && releases.member?(r)
139
+ (h[i.claimer] ||= []) << i
140
+ end
141
+ h
142
+ end
143
+
144
+ if issues.empty?
145
+ puts "No issues."
146
+ else
147
+ issues.keys.sort.each do |c|
148
+ puts "#{c}:"
149
+ puts todo_list_for(issues[c], :show_release => true)
150
+ puts
151
+ end
152
+ end
153
+ end
154
+
155
+ operation :unclaimed, "Show all unclaimed issues", :maybe_release do
156
+ opt :all, "Show all issues, not just open ones"
157
+ end
158
+ def unclaimed project, config, opts, releases
159
+ releases ||= project.unreleased_releases + [:unassigned]
160
+ releases = [*releases]
161
+
162
+ issues = project.issues.select do |i|
163
+ r = project.release_for(i.release) || :unassigned
164
+ releases.member?(r) && i.claimer.nil? && (opts[:all] || i.open?)
165
+ end
166
+ if issues.empty?
167
+ puts "No issues."
168
+ else
169
+ print_todo_list_by_release_for project, issues
170
+ end
171
+ end
172
+ end
173
+
174
+ end
@@ -0,0 +1,161 @@
1
+ ## issue-labeling ditz plugin
2
+ ##
3
+ ## This plugin allows label issues. This can replace the issue component
4
+ ## and/or issue types (bug,feature,task), by providing a more flexible
5
+ ## to organize your issues.
6
+ ##
7
+ ## Commands added:
8
+ ## ditz new_label [label]: create a new label for the project
9
+ ## ditz label <issue> <labels>: label an issue with some labels
10
+ ## ditz unlabel <issue> [labels]: remove some label(s) of an issue
11
+ ## ditz labeled <labels> [release]: show all issues with these labels
12
+ ##
13
+ ## Usage:
14
+ ## 1. add a line "- issue-labeling" to the .ditz-plugins file in the project
15
+ ## root
16
+ ## 2. use the above commands to abandon
17
+
18
+ # TODO:
19
+ # * extend the HTML view to have per-labels listings
20
+ # * allow for more compact way to type them (completion, prefixes...)
21
+
22
+ module Ditz
23
+
24
+ class Label < ModelObject
25
+ include Comparable
26
+ field :name
27
+
28
+ def name_prefix; @name.gsub(/\s+/, "-").downcase end
29
+
30
+ def <=> x ; name <=> x.name end
31
+ def == x ; name == x.name end
32
+
33
+ end # class Label
34
+
35
+ class Project
36
+ field :labels, :multi => true, :interactive_generator => :get_labels
37
+
38
+ def get_labels
39
+ lab_names = ask_for_many("labels")
40
+ ([name] + lab_names).uniq.map { |n| Label.create_interactively :with => { :name => n } }
41
+ end
42
+
43
+ def label_for label_name
44
+ labels.find { |i| i.name == label_name }
45
+ end
46
+
47
+ def labels_for label_names
48
+ label_names.split(/\s*,\s*/).map do |val|
49
+ label_for(val) or raise Error, "no label with name #{val}"
50
+ end
51
+ end
52
+
53
+ end # class Project
54
+
55
+ class Issue
56
+ field :labels, :multi => true, :interactive_generator => :get_labels
57
+
58
+ def get_labels config, project
59
+ ask_for_selection(project.labels, "label", :name, true).map {|x|x.name}
60
+ end
61
+
62
+ def apply_labels new_labels, who, comment
63
+ log "issue labeled", who, comment
64
+ new_labels.each { |l| add_label l }
65
+ end
66
+
67
+ def remove_labels labels_to_remove, who, comment
68
+ log "issue unlabeled", who, comment
69
+ if labels_to_remove.nil?
70
+ self.labels = []
71
+ else
72
+ labels_to_remove.each { |l| drop_label l }
73
+ end
74
+ end
75
+
76
+ def labeled? label=nil; (label.nil?)? !labels.empty? : labels.include?(label) end
77
+ def unlabeled? label=nil; !labeled?(label) end
78
+ end
79
+
80
+ class ScreenView
81
+ add_to_view :issue_summary do |issue, config|
82
+ " Labels: #{(issue.labeled?)? issue.labels.map{|l|l.name}.join(', ') : 'none'}\n"
83
+ end
84
+ end
85
+
86
+ class HtmlView
87
+ add_to_view :issue_summary do |issue, config|
88
+ next if issue.unlabeled?
89
+ [<<EOS, { :issue => issue }]
90
+ <tr>
91
+ <td class='attrname'>Labels:</td>
92
+ <td class='attrval'><%= h(issue.labels.map{|l|l.name}.join(', ')) %></td>
93
+ </tr>
94
+ EOS
95
+ end
96
+ end
97
+
98
+ class Operator
99
+
100
+ operation :new_label, "Create a new label for the project", :maybe_label
101
+ def new_label project, config, maybe_label
102
+ puts "Adding label #{maybe_label}." if maybe_label
103
+ label = Label.create_interactively(:args => [project, config], :with => { :name => maybe_label }) or return
104
+ project.add_label label
105
+ puts "Added label #{label.name}."
106
+ end
107
+
108
+ operation :label, "Apply labels to an issue", :issue, :labels
109
+ def label project, config, issue, label_names
110
+ labels = project.labels_for label_names
111
+ puts "Adding labels #{label_names} to issue #{issue.name}: #{issue.title}."
112
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
113
+ issue.apply_labels labels, config.user, comment
114
+ show_labels issue
115
+ end
116
+
117
+ operation :unlabel, "Remove some labels of an issue", :issue, :maybe_labels
118
+ def unlabel project, config, issue, label_names
119
+ labels = if label_names
120
+ puts "Removing #{label_names} labels from issue #{issue.name}: #{issue.title}."
121
+ project.labels_for label_names
122
+ else
123
+ puts "Removing labels from issue #{issue.name}: #{issue.title}."
124
+ nil
125
+ end
126
+ comment = ask_multiline "Comments" unless $opts[:no_comment]
127
+ issue.remove_labels labels, config.user, comment
128
+ show_labels issue
129
+ end
130
+
131
+ def show_labels issue
132
+ if issue.labeled?
133
+ puts "Issue #{issue.name} is now labeled with #{issue.labels.map{|l|l.name}.join(', ')}"
134
+ else
135
+ puts "Issue #{issue.name} is now unlabeled"
136
+ end
137
+ end
138
+ private :show_labels
139
+
140
+ operation :labeled, "Show labeled issues", :labels, :maybe_release do
141
+ opt :all, "Show all issues, not just open ones"
142
+ end
143
+ def labeled project, config, opts, labels, releases
144
+ releases ||= project.unreleased_releases + [:unassigned]
145
+ releases = [*releases]
146
+ labels = project.labels_for labels
147
+
148
+ issues = project.issues.select do |i|
149
+ r = project.release_for(i.release) || :unassigned
150
+ labels.all? { |l| i.labeled? l } && (opts[:all] || i.open?) && releases.member?(r)
151
+ end
152
+
153
+ if issues.empty?
154
+ puts "No issues."
155
+ else
156
+ print_todo_list_by_release_for project, issues
157
+ end
158
+ end
159
+ end
160
+
161
+ end