ditz 0.3 → 0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Changelog +9 -0
- data/README.txt +22 -11
- data/Rakefile +1 -15
- data/ReleaseNotes +29 -0
- data/bin/ditz +32 -20
- data/contrib/completion/_ditz.zsh +29 -0
- data/contrib/completion/ditz.bash +22 -0
- data/lib/ditz.rb +29 -2
- data/lib/hook.rb +9 -2
- data/lib/html.rb +36 -14
- data/lib/index.rhtml +24 -1
- data/lib/issue.rhtml +6 -1
- data/lib/issue_table.rhtml +6 -2
- data/lib/lowline.rb +1 -8
- data/lib/model-objects.rb +7 -2
- data/lib/model.rb +12 -4
- data/lib/operator.rb +81 -166
- data/lib/plugins/git.rb +101 -0
- data/lib/style.css +39 -3
- data/lib/unassigned.rhtml +1 -1
- data/lib/util.rb +5 -0
- data/lib/view.rb +16 -0
- data/lib/views.rb +136 -0
- metadata +22 -8
- data/bin/ditz-convert-from-monolith +0 -42
data/lib/plugins/git.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Ditz
|
4
|
+
class Issue
|
5
|
+
field :git_branch, :ask => false
|
6
|
+
|
7
|
+
def git_commits
|
8
|
+
return @git_commits if @git_commits
|
9
|
+
|
10
|
+
filters = ["--grep=\"Ditz-issue: #{id}\""]
|
11
|
+
filters << "master..#{git_branch}" if git_branch
|
12
|
+
|
13
|
+
output = filters.map do |f|
|
14
|
+
`git log --pretty=format:\"%aD\t%an <%ae>\t%h\t%s\" #{f}`
|
15
|
+
end.join
|
16
|
+
|
17
|
+
@git_commits = output.split(/\n/).map { |l| l.split("\t") }.
|
18
|
+
map { |date, email, hash, msg| [Time.parse(date).utc, email, hash, msg] }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
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"
|
25
|
+
end
|
26
|
+
|
27
|
+
class ScreenView
|
28
|
+
add_to_view :issue_summary do |issue, config|
|
29
|
+
" Git branch: #{issue.git_branch || 'none'}\n"
|
30
|
+
end
|
31
|
+
|
32
|
+
add_to_view :issue_details do |issue, config|
|
33
|
+
commits = issue.git_commits[0...5]
|
34
|
+
next if commits.empty?
|
35
|
+
"Recent commits:\n" + commits.map do |date, email, hash, msg|
|
36
|
+
"- #{msg} [#{hash}] (#{email.shortened_email}, #{date.ago} ago)\n"
|
37
|
+
end.join + "\n"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class HtmlView
|
42
|
+
add_to_view :issue_summary do |issue, config|
|
43
|
+
next unless issue.git_branch
|
44
|
+
[<<EOS, { :issue => issue, :url_prefix => config.git_branch_url_prefix }]
|
45
|
+
<tr>
|
46
|
+
<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>
|
48
|
+
</tr>
|
49
|
+
EOS
|
50
|
+
end
|
51
|
+
|
52
|
+
add_to_view :issue_details do |issue, config|
|
53
|
+
commits = issue.git_commits
|
54
|
+
next if commits.empty?
|
55
|
+
|
56
|
+
[<<EOS, { :commits => commits, :url_prefix => config.git_commit_url_prefix }]
|
57
|
+
<h2>Commits for this issue</h2>
|
58
|
+
<table>
|
59
|
+
<% 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>
|
68
|
+
</tr>
|
69
|
+
<% end %>
|
70
|
+
</table>
|
71
|
+
EOS
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Operator
|
76
|
+
operation :set_branch, "Set the git feature branch of an issue", :issue, :maybe_string
|
77
|
+
def set_branch project, config, issue, maybe_string
|
78
|
+
puts "Issue #{issue.name} currently " + if issue.git_branch
|
79
|
+
"assigned to git branch #{issue.git_branch.inspect}."
|
80
|
+
else
|
81
|
+
"not assigned to any git branch."
|
82
|
+
end
|
83
|
+
|
84
|
+
branch = maybe_string || ask("Git feature branch name:")
|
85
|
+
return unless branch
|
86
|
+
|
87
|
+
if branch == issue.git_branch
|
88
|
+
raise Error, "issue #{issue.name} already assigned to branch #{issue.git_branch.inspect}"
|
89
|
+
end
|
90
|
+
|
91
|
+
puts "Assigning to branch #{branch.inspect}."
|
92
|
+
issue.git_branch = branch
|
93
|
+
end
|
94
|
+
|
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"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
data/lib/style.css
CHANGED
@@ -7,11 +7,17 @@ body {
|
|
7
7
|
width: 90%;
|
8
8
|
}
|
9
9
|
|
10
|
+
div.wrapper {
|
11
|
+
}
|
12
|
+
|
13
|
+
div.footer {
|
14
|
+
}
|
15
|
+
|
10
16
|
a, a:visited {
|
11
17
|
border-bottom: 1px dotted #03c;
|
12
18
|
background: inherit;
|
13
|
-
color: #03c;
|
14
19
|
text-decoration: none;
|
20
|
+
color: #03c;
|
15
21
|
}
|
16
22
|
|
17
23
|
ul {
|
@@ -23,6 +29,20 @@ p {
|
|
23
29
|
width: 40em;
|
24
30
|
}
|
25
31
|
|
32
|
+
.dimmed {
|
33
|
+
color: #bbb;
|
34
|
+
}
|
35
|
+
|
36
|
+
div.on-the-left {
|
37
|
+
float: left;
|
38
|
+
width: 39%;
|
39
|
+
}
|
40
|
+
|
41
|
+
div.on-the-right {
|
42
|
+
float: right;
|
43
|
+
width: 59%;
|
44
|
+
}
|
45
|
+
|
26
46
|
table {
|
27
47
|
border-style: none;
|
28
48
|
border-spacing: 0;
|
@@ -70,8 +90,23 @@ h1 {
|
|
70
90
|
text-align: center;
|
71
91
|
}
|
72
92
|
|
93
|
+
.issuestatus_closed a {
|
94
|
+
color: #202020;
|
95
|
+
}
|
96
|
+
|
97
|
+
.issuestatus_in_progress a {
|
98
|
+
color: #202020;
|
99
|
+
}
|
100
|
+
|
101
|
+
.issuestatus_paused a {
|
102
|
+
color: #202020;
|
103
|
+
}
|
104
|
+
|
105
|
+
.issuestatus_unstarted a {
|
106
|
+
color: #202020;
|
107
|
+
}
|
108
|
+
|
73
109
|
.logwhat {
|
74
|
-
font-style: italic;
|
75
110
|
}
|
76
111
|
|
77
112
|
.logentryeven {
|
@@ -86,6 +121,7 @@ h1 {
|
|
86
121
|
padding-left: 3em;
|
87
122
|
}
|
88
123
|
|
89
|
-
.footer {
|
124
|
+
p.footer {
|
90
125
|
font-size: small;
|
91
126
|
}
|
127
|
+
|
data/lib/unassigned.rhtml
CHANGED
data/lib/util.rb
CHANGED
@@ -8,6 +8,10 @@ module Enumerable
|
|
8
8
|
def min_of(&b); map(&b).min end
|
9
9
|
end
|
10
10
|
|
11
|
+
class Array
|
12
|
+
def uniq_by; inject({}) { |h, o| h[yield(o)] = o; h }.values end
|
13
|
+
end
|
14
|
+
|
11
15
|
module Enumerable
|
12
16
|
def map_with_index # sigh...
|
13
17
|
ret = []
|
@@ -50,3 +54,4 @@ class Array
|
|
50
54
|
end
|
51
55
|
end
|
52
56
|
end
|
57
|
+
|
data/lib/view.rb
ADDED
data/lib/views.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'view'
|
2
|
+
require 'html'
|
3
|
+
|
4
|
+
module Ditz
|
5
|
+
|
6
|
+
class ScreenView < View
|
7
|
+
def initialize project, config, device=$stdout
|
8
|
+
@device = device
|
9
|
+
@config = config
|
10
|
+
end
|
11
|
+
|
12
|
+
def format_log_events events
|
13
|
+
return "none" if events.empty?
|
14
|
+
events.reverse.map do |time, who, what, comment|
|
15
|
+
"- #{what} (#{who.shortened_email}, #{time.ago} ago)" +
|
16
|
+
(comment =~ /\S/ ? "\n" + comment.gsub(/^/, " > ") : "")
|
17
|
+
end.join("\n")
|
18
|
+
end
|
19
|
+
private :format_log_events
|
20
|
+
|
21
|
+
def render_issue issue
|
22
|
+
status = case issue.status
|
23
|
+
when :closed
|
24
|
+
"#{issue.status_string}: #{issue.disposition_string}"
|
25
|
+
else
|
26
|
+
issue.status_string
|
27
|
+
end
|
28
|
+
desc = if issue.desc.size < 80 - "Description: ".length
|
29
|
+
issue.desc
|
30
|
+
else
|
31
|
+
"\n" + issue.desc.gsub(/^/, " ") + "\n"
|
32
|
+
end
|
33
|
+
@device.puts <<EOS
|
34
|
+
#{"Issue #{issue.name}".underline}
|
35
|
+
Title: #{issue.title}
|
36
|
+
Description: #{desc}
|
37
|
+
Type: #{issue.type}
|
38
|
+
Status: #{status}
|
39
|
+
Creator: #{issue.reporter}
|
40
|
+
Age: #{issue.creation_time.ago}
|
41
|
+
Release: #{issue.release}
|
42
|
+
References: #{issue.references.listify " "}
|
43
|
+
Identifier: #{issue.id}
|
44
|
+
EOS
|
45
|
+
|
46
|
+
self.class.view_additions_for(:issue_summary).each { |b| @device.print(b[issue, @config] || next) }
|
47
|
+
puts
|
48
|
+
self.class.view_additions_for(:issue_details).each { |b| @device.print(b[issue, @config] || next) }
|
49
|
+
|
50
|
+
@device.puts <<EOS
|
51
|
+
Event log:
|
52
|
+
#{format_log_events issue.log_events}
|
53
|
+
EOS
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class HtmlView < View
|
58
|
+
def initialize project, config, dir
|
59
|
+
@project = project
|
60
|
+
@config = config
|
61
|
+
@dir = dir
|
62
|
+
@template_dir = File.dirname Ditz::find_ditz_file("index.rhtml")
|
63
|
+
end
|
64
|
+
|
65
|
+
def render_all
|
66
|
+
Dir.mkdir @dir unless File.exists? @dir
|
67
|
+
FileUtils.cp File.join(@template_dir, "style.css"), @dir
|
68
|
+
|
69
|
+
## build up links
|
70
|
+
links = {}
|
71
|
+
@project.releases.each { |r| links[r] = "release-#{r.name}.html" }
|
72
|
+
@project.issues.each { |i| links[i] = "issue-#{i.id}.html" }
|
73
|
+
@project.components.each { |c| links[c] = "component-#{c.name}.html" }
|
74
|
+
links["unassigned"] = "unassigned.html" # special case: unassigned
|
75
|
+
links["index"] = "index.html" # special case: index
|
76
|
+
|
77
|
+
@project.issues.each do |issue|
|
78
|
+
fn = File.join @dir, links[issue]
|
79
|
+
#puts "Generating #{fn}..."
|
80
|
+
|
81
|
+
extra_summary = self.class.view_additions_for(:issue_summary).map { |b| b[issue, @config] }.compact
|
82
|
+
extra_details = self.class.view_additions_for(:issue_details).map { |b| b[issue, @config] }.compact
|
83
|
+
|
84
|
+
erb = ErbHtml.new(@template_dir, links, :issue => issue,
|
85
|
+
:release => (issue.release ? @project.release_for(issue.release) : nil),
|
86
|
+
:component => @project.component_for(issue.component),
|
87
|
+
:project => @project)
|
88
|
+
|
89
|
+
extra_summary_html = extra_summary.map { |string, extra_binding| erb.render_string string, extra_binding }.join
|
90
|
+
extra_details_html = extra_details.map { |string, extra_binding| erb.render_string string, extra_binding }.join
|
91
|
+
|
92
|
+
File.open(fn, "w") { |f| f.puts erb.render_template("issue", { :extra_summary_html => extra_summary_html, :extra_details_html => extra_details_html }) }
|
93
|
+
end
|
94
|
+
|
95
|
+
@project.releases.each do |r|
|
96
|
+
fn = File.join @dir, links[r]
|
97
|
+
#puts "Generating #{fn}..."
|
98
|
+
File.open(fn, "w") do |f|
|
99
|
+
f.puts ErbHtml.new(@template_dir, links, :release => r,
|
100
|
+
:issues => @project.issues_for_release(r), :project => @project).
|
101
|
+
render_template("release")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
@project.components.each do |c|
|
106
|
+
fn = File.join @dir, links[c]
|
107
|
+
#puts "Generating #{fn}..."
|
108
|
+
File.open(fn, "w") do |f|
|
109
|
+
f.puts ErbHtml.new(@template_dir, links, :component => c,
|
110
|
+
:issues => @project.issues_for_component(c), :project => @project).
|
111
|
+
render_template("component")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
fn = File.join @dir, links["unassigned"]
|
116
|
+
#puts "Generating #{fn}..."
|
117
|
+
File.open(fn, "w") do |f|
|
118
|
+
f.puts ErbHtml.new(@template_dir, links,
|
119
|
+
:issues => @project.unassigned_issues, :project => @project).
|
120
|
+
render_template("unassigned")
|
121
|
+
end
|
122
|
+
|
123
|
+
past_rels, upcoming_rels = @project.releases.partition { |r| r.released? }
|
124
|
+
fn = File.join @dir, links["index"]
|
125
|
+
#puts "Generating #{fn}..."
|
126
|
+
File.open(fn, "w") do |f|
|
127
|
+
f.puts ErbHtml.new(@template_dir, links, :project => @project,
|
128
|
+
:past_releases => past_rels, :upcoming_releases => upcoming_rels,
|
129
|
+
:components => @project.components).
|
130
|
+
render_template("index")
|
131
|
+
end
|
132
|
+
puts "Local generated URL: file://#{File.expand_path(fn)}"
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ditz
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: "0.
|
4
|
+
version: "0.4"
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- William Morgan
|
@@ -9,23 +9,33 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-07-27 07:00:00 Z
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: trollop
|
17
|
+
type: :runtime
|
17
18
|
version_requirement:
|
18
19
|
version_requirements: !ruby/object:Gem::Requirement
|
19
20
|
requirements:
|
20
21
|
- - ">="
|
21
22
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
23
|
+
version: 1.8.2
|
23
24
|
version:
|
24
|
-
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: hoe
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.7.0
|
34
|
+
version:
|
35
|
+
description: "Ditz is a simple, light-weight distributed issue tracker designed to work with distributed version control systems like git, darcs, Mercurial, and Bazaar. It can also be used with centralized systems like SVN. Ditz maintains an issue database directory on disk, with files written in a line-based and human-editable format. This directory can be kept under version control, alongside project code. There are different ways to use ditz: 1. Treat issue change the same as code change: include it as part of commits, and merge it with changes from other developers. (Resolving conflicts in the usual manner.) 2. Keep the issue database in the repository but in a separate branch. Issue changes can be managed by your VCS, but is not tied directly to commits. 3. Keep the issue database separate and not under VCS at all. Your particular usage will depend on what you want to get out of ditz. Ditz provides a simple, console-based interface for creating and updating the issue database file, and some rudimentary HTML generation capabilities for producing world-readable status pages. It currently offers no central public method of bug submission. Synopsis: # set up project. creates the bugs.yaml file. 1. ditz init 2. ditz add-release"
|
25
36
|
email: wmorgan-ditz@masanjin.net
|
26
37
|
executables:
|
27
38
|
- ditz
|
28
|
-
- ditz-convert-from-monolith
|
29
39
|
extensions: []
|
30
40
|
|
31
41
|
extra_rdoc_files:
|
@@ -36,9 +46,9 @@ files:
|
|
36
46
|
- Rakefile
|
37
47
|
- ReleaseNotes
|
38
48
|
- bin/ditz
|
39
|
-
- bin/ditz-convert-from-monolith
|
40
49
|
- lib/component.rhtml
|
41
50
|
- lib/ditz.rb
|
51
|
+
- lib/hook.rb
|
42
52
|
- lib/html.rb
|
43
53
|
- lib/index.rhtml
|
44
54
|
- lib/issue.rhtml
|
@@ -51,7 +61,11 @@ files:
|
|
51
61
|
- lib/style.css
|
52
62
|
- lib/unassigned.rhtml
|
53
63
|
- lib/util.rb
|
54
|
-
- lib/
|
64
|
+
- lib/view.rb
|
65
|
+
- lib/views.rb
|
66
|
+
- lib/plugins/git.rb
|
67
|
+
- contrib/completion/ditz.bash
|
68
|
+
- contrib/completion/_ditz.zsh
|
55
69
|
has_rdoc: true
|
56
70
|
homepage: http://ditz.rubyforge.org
|
57
71
|
post_install_message:
|
@@ -75,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
75
89
|
requirements: []
|
76
90
|
|
77
91
|
rubyforge_project: ditz
|
78
|
-
rubygems_version: 1.
|
92
|
+
rubygems_version: 1.2.0
|
79
93
|
signing_key:
|
80
94
|
specification_version: 2
|
81
95
|
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.
|
@@ -1,42 +0,0 @@
|
|
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."
|