ditz 0.4 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -20,8 +48,8 @@ class Issue
20
48
  end
21
49
 
22
50
  class Config
23
- field :git_commit_url_prefix, :prompt => "URL prefix (if any) to link git commits to"
24
- field :git_branch_url_prefix, :prompt => "URL prefix (if any) to link git branches to"
51
+ field :git_commit_url_prefix, :prompt => "URL prefix to link git commits to", :default => ""
52
+ field :git_branch_url_prefix, :prompt => "URL prefix to link git branches to", :default => ""
25
53
  end
26
54
 
27
55
  class ScreenView
@@ -44,7 +72,7 @@ class HtmlView
44
72
  [<<EOS, { :issue => issue, :url_prefix => config.git_branch_url_prefix }]
45
73
  <tr>
46
74
  <td class='attrname'>Git branch:</td>
47
- <td class='attrval'><%= url_prefix ? link_to([url_prefix, issue.git_branch].join, issue.git_branch) : h(issue.git_branch) %></td>
75
+ <td class='attrval'><%= url_prefix && !url_prefix.blank? ? link_to([url_prefix, issue.git_branch].join, issue.git_branch) : h(issue.git_branch) %></td>
48
76
  </tr>
49
77
  EOS
50
78
  end
@@ -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 ? 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
@@ -92,9 +117,36 @@ class Operator
92
117
  issue.git_branch = branch
93
118
  end
94
119
 
95
- operation :commit, "Runs git-commit and auto-fills the issue name in the commit message", :issue
96
- def commit project, config, issue
97
- exec "git commit -v -m \"Ditz-issue: #{issue.id}\" -e"
120
+ operation :commit, "Runs git-commit and auto-fills the issue name in the commit message", :issue do
121
+ opt :all, "commit all changed files", :short => "-a", :default => false
122
+ opt :verbose, "show diff between HEAD and what would be committed", \
123
+ :short => "-v", :default => false
124
+ opt :message, "Use the given <s> as the commit message.", \
125
+ :short => "-m", :type => :string
126
+ opt :edit, "Further edit the message, even if --message is given.", :short => "-e", :default => false
127
+ end
128
+
129
+ def commit project, config, opts, issue
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}\""
98
150
  end
99
151
  end
100
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
Binary file
@@ -1,37 +1,48 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
3
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4
+
1
5
  <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
2
6
  <head>
3
7
  <title><%= project.name %> release <%= release.name %></title>
4
- <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
8
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5
9
  <link rel="stylesheet" href="style.css" type="text/css" />
6
10
  </head>
7
11
  <body>
8
12
 
9
- <%= link_to "index", "&laquo; #{project.name} project page" %>
13
+ <div class="main">
10
14
 
11
15
  <h1><%= project.name %> release <%= release.name %></h1>
16
+ <div class="backptr"><%= link_to "index", "&laquo; #{project.name} project page" %></div>
12
17
 
13
- <p>
14
18
  <table>
15
- <tr>
16
- <td class="attrname">Status:</td>
17
- <td class="attrval"><%= release.status %></td>
18
- </tr>
19
- <% if release.released? %>
19
+ <tbody>
20
20
  <tr>
21
- <td class="attrname">Release time:</td>
22
- <td class="attrval"><%= release.release_time %></td>
21
+ <td class="attrname">Status:</td>
22
+ <td class="attrval"><%= release.status %></td>
23
23
  </tr>
24
- <% end %>
25
- <tr>
26
- <%
27
- num_done = issues.count_of { |i| i.closed? }
28
- pct_done = issues.size == 0 ? 100 : (100.0 * num_done / issues.size)
29
- %>
30
- <td class="attrname">Completion:</td>
31
- <td class="attrval"><%= sprintf "%.0f%%", pct_done %></td>
32
- </tr>
24
+ <% if release.released? %>
25
+ <tr>
26
+ <td class="attrname">Release time:</td>
27
+ <td class="attrval"><%=t release.release_time %></td>
28
+ </tr>
29
+ <% end %>
30
+ <tr>
31
+ <td class="attrname">Completion:</td>
32
+ <td>
33
+ <%
34
+ num_done = issues.count_of { |i| i.closed? }
35
+ pct_done = issues.size == 0 ? 1.0 : (num_done.to_f / issues.size.to_f)
36
+ %>
37
+ <%= progress_meter pct_done %>
38
+ <%= sprintf "%.0f%%", pct_done * 100.0 %>
39
+ </td>
40
+ </tr>
41
+ <tr><td></td><td class="attrval">
42
+ <%= num_done %> / <%= issues.size %> issues
43
+ </td></tr>
44
+ </tbody>
33
45
  </table>
34
- </p>
35
46
 
36
47
  <h2>Issues</h2>
37
48
  <% if issues.empty? %>
@@ -40,28 +51,48 @@
40
51
  <%= render "issue_table", :show_component => false, :show_release => false %>
41
52
  <% end %>
42
53
 
54
+ <h2>Recent activity for this release</h2>
55
+ <table class="log">
56
+ <tbody>
57
+ <% issues.map { |i| i.log_events.map { |e| [e, i] } }.
58
+ flatten_one_level.
59
+ sort_by { |e| e.first.first }.
60
+ reverse[0 ... 10].
61
+ each_with_index do |((date, who, what, comment), i), idx| %>
62
+ <tr class="<%= idx % 2 == 0 ? "even-row" : "odd-row" %>">
63
+ <td class="date"><%= date.pretty_date %></td>
64
+ <td class="issuename">
65
+ <%= issue_link_for i, :status_image => true %>
66
+ </td>
67
+ <td> <%= what %> </td>
68
+ </tr>
69
+ <tr><td></td></tr>
70
+ <% end %>
71
+ </tbody>
72
+ </table>
73
+
43
74
  <h2>Release log</h2>
44
- <table>
45
- <% release.log_events.each_with_index do |(time, who, what, comment), i| %>
46
- <% if i % 2 == 0 %>
47
- <tr class="logentryeven">
48
- <% else %>
49
- <tr class="logentryodd">
50
- <% end %>
51
- <td class="logtime"><%=h time %></td>
52
- <td class="logwho"><%=obscured_email who %></td>
53
- <td class="logwhat"><%=h what %></td>
54
- </tr>
55
- <tr><td colspan="3" class="logcomment">
56
- <% if comment.empty? %>
57
- <% else %>
58
- <%= link_issue_names project, p(comment) %>
59
- <% end %>
60
- </td></tr>
75
+ <table class="log">
76
+ <tbody>
77
+ <% release.log_events.reverse.each_with_index do |(time, who, what, comment), i| %>
78
+ <tr class="<%= i % 2 == 0 ? "even-row" : "odd-row" %>">
79
+ <td class="date"><%=h time %></td>
80
+ <td class="person"><%=obscured_email who %></td>
81
+ <td class="message"><%=h what %></td>
82
+ </tr>
83
+ <tr><td colspan="3" class="logcomment">
84
+ <% if comment.empty? %>
85
+ <% else %>
86
+ <%= link_issue_names project, comment %>
87
+ <% end %>
88
+ </td></tr>
89
+ <tr><td></td></tr>
61
90
  <% end %>
91
+ </tbody>
62
92
  </table>
63
93
 
64
- <p class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.
94
+ </div>
95
+ <div class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.</div>
65
96
 
66
97
  </body>
67
98
  </html>