ditz 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ == 0.1 / 2008-04-02
2
+ * Initial release!
@@ -0,0 +1,117 @@
1
+ == ditz
2
+
3
+ by William Morgan <wmorgan-ditz@masanjin.net>
4
+
5
+ http://ditz.rubyforge.org
6
+
7
+ == DESCRIPTION
8
+
9
+ Ditz is a simple, light-weight distributed issue tracker designed to work with
10
+ distributed version control systems like darcs and git. Ditz maintains an issue
11
+ database file on disk, written in a line-based and human-editable format. This
12
+ file can be kept under version control alongside project code, and issue state
13
+ change can then be handled like code changes: modified as part of a commit,
14
+ merged with other state changes from other developers, conflict-resolved in the
15
+ standard manner, etc.
16
+
17
+ Ditz provides a simple, console-based interface for creating and updating the
18
+ issue database file, and some rudimentary HTML generation capabilities for
19
+ producing world-readable status pages. It offers no central public method of
20
+ bug submission.
21
+
22
+ == SYNOPSIS
23
+
24
+ # set up project. creates the bugs.yaml file.
25
+ 1. ditz init
26
+ 2. ditz add-release
27
+
28
+ # add an issue
29
+ 3. ditz add
30
+
31
+ # where am i?
32
+ 4. ditz status
33
+ 5. ditz todo
34
+
35
+ # do work
36
+ 6. write code
37
+ 7. ditz close <issue-id>
38
+ 8. commit
39
+ 9. goto 3
40
+
41
+ # finished!
42
+ 10. ditz release <release-name>
43
+
44
+ == THE DITZ DATA MODEL
45
+
46
+ Ditz includes the bare minimum set of features necessary for open-source
47
+ development. Features like time spent, priority, assignment of tasks to
48
+ developers, due dates, etc. are purposely excluded.
49
+
50
+ A ditz project consists of issues, releases and components.
51
+
52
+ Issues:
53
+ Issues are the fundamental currency of issue tracking. A ditz issue is either
54
+ a feature or a bug, but this distinction doesn't affect anything other than
55
+ how they're displayed.
56
+
57
+ Each issue belongs to exactly one component, and is part of zero or one
58
+ releases.
59
+
60
+ Each issues has an exportable id, in the form of 40 random hex characters.
61
+ This id is "guaranteed" to be unique across all possible issues and
62
+ developers, present and future. Issue ids are typically not exposed to the
63
+ user.
64
+
65
+ Issues also have a non-exportable name, which is short and human-readable.
66
+ All ditz commands use issue names instead of issue ids. Issue ids may change
67
+ in certain circumstances, specifically after a "ditz drop" command.
68
+
69
+ Components:
70
+ There is always one "general" component, named after the project itself. In
71
+ the simplest case, this is the only component, and the user is never bothered
72
+ with the question of which component to assign an issue to.
73
+
74
+ Components simply provide a way of organizing issues, and have no real
75
+ functionality. Issues are assigned names derived form the component they're
76
+ assigned to.
77
+
78
+ Releases:
79
+ A release is the primary grouping mechanism for issues. Status commands like
80
+ "ditz status" and "ditz todo" always group issues by release. When a release
81
+ is 100% complete, it can be marked as released, in which case the associated
82
+ issues will cease appearing in ditz status and todo messages.
83
+
84
+ == FUTURE WORK
85
+
86
+ In future releases, Ditz will have a plugin architecture to allow tighter
87
+ integration with specific SCMs and developer communication channels. (See
88
+ http://ditz.rubyforge.org/ditz/issue-0704dafe4aef96279364013aba177a0971d425cb.html)
89
+
90
+ == LEARNING MORE
91
+
92
+ * ditz help
93
+ * find $DITZ_INSTALL_DIR -type f | xargs cat
94
+
95
+ == REQUIREMENTS
96
+
97
+ * trollop >= 1.7
98
+
99
+ == INSTALLATION
100
+
101
+ Download tarballs from http://rubyforge.org/projects/ditz/, or command your
102
+ computer to "gem install ditz".
103
+
104
+ == LICENSE
105
+
106
+ Copyright (c) 2008 William Morgan.
107
+
108
+ This program is free software: you can redistribute it and/or modify
109
+ it under the terms of the GNU General Public License as published by
110
+ the Free Software Foundation, either version 3 of the License, or
111
+ (at your option) any later version.
112
+
113
+ This program is distributed in the hope that it will be useful,
114
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
115
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
116
+ GNU General Public License for more details.
117
+
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+
4
+ $:.unshift "lib"
5
+ require 'ditz'
6
+
7
+ class Hoe
8
+ def extra_deps; @extra_deps.reject { |x| Array(x).first == "hoe" } end
9
+ end # thanks to "Mike H"
10
+
11
+ Hoe.new('ditz', Ditz::VERSION) do |p|
12
+ p.rubyforge_name = 'ditz'
13
+ p.author = "William Morgan"
14
+ p.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."
15
+
16
+ p.description = p.paragraphs_of('README.txt', 4..5, 9..18).join("\n\n").gsub(/== SYNOPSIS/, "Synopsis")
17
+ p.url = "http://ditz.rubyforge.org"
18
+ p.changes = p.paragraphs_of('Changelog', 0..0).join("\n\n")
19
+ p.email = "wmorgan-ditz@masanjin.net"
20
+ p.extra_deps = [['trollop', '>= 1.7']]
21
+ end
22
+
23
+ WWW_FILES = FileList["www/*"] + %w(README.txt)
24
+ SCREENSHOTS = FileList["www/ss?.png"]
25
+ SCREENSHOTS_SMALL = []
26
+ SCREENSHOTS.each do |fn|
27
+ fn =~ /ss(\d+)\.png/
28
+ sfn = "www/ss#{$1}-small.png"
29
+ file sfn => [fn] do |t|
30
+ sh "cat #{fn} | pngtopnm | pnmscale -xysize 320 240 | pnmtopng > #{sfn}"
31
+ end
32
+ SCREENSHOTS_SMALL << sfn
33
+ end
34
+
35
+ task :upload_webpage => WWW_FILES do |t|
36
+ sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
37
+ end
38
+
39
+ task :upload_webpage_images => (SCREENSHOTS + SCREENSHOTS_SMALL) do |t|
40
+ sh "scp -C #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
41
+ end
42
+
43
+ task :upload_report do |t|
44
+ sh "ditz html ditz"
45
+ sh "scp -Cr ditz wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
46
+ end
47
+
48
+ # vim: syntax=ruby
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'trollop'; include Trollop
5
+ require "ditz"
6
+
7
+ $opts = options do
8
+ version "ditz #{Ditz::VERSION} (c) 2008 William Morgan"
9
+ opt :issue_file, "Issue database file", :default => "bugs.yaml"
10
+ opt :config_file, "Configuration file", :default => File.join(ENV["HOME"], ".ditz-config")
11
+ opt :verbose, "Verbose output", :default => false
12
+ end
13
+
14
+ cmd = ARGV.shift or die "expecting a ditz command"
15
+ op = Ditz::Operator.new
16
+
17
+ case cmd # special cases: init and help
18
+ when "init"
19
+ fn = $opts[:issue_file]
20
+ die "#{fn} already exists" if File.exists? fn
21
+ project = op.init
22
+ project.save! fn
23
+ puts "Ok, #{fn} created successfully."
24
+ exit
25
+ when "help"
26
+ op.help
27
+ exit
28
+ end
29
+
30
+ Ditz::debug "loading issues from #{$opts[:issue_file]}"
31
+ project = begin
32
+ Ditz::Project.from $opts[:issue_file]
33
+ rescue SystemCallError, Ditz::Project::Error => e
34
+ die "#{e.message} (use 'init' to initialize)"
35
+ end
36
+
37
+ project.validate!
38
+ project.assign_issue_names!
39
+ project.each_modelobject { |o| o.after_deserialize project }
40
+
41
+ config = begin
42
+ fn = ".ditz-config"
43
+ if File.exists? fn
44
+ Ditz::debug "loading config from #{fn}"
45
+ Ditz::Config.from fn
46
+ else
47
+ Ditz::debug "loading config from #{$opts[:config_file]}"
48
+ Ditz::Config.from $opts[:config_file]
49
+ end
50
+ rescue SystemCallError, Ditz::ModelObject::ModelError => e
51
+ puts <<EOS
52
+ I wasn't able to find a configuration file #{$opts[:config_file]}.
53
+ We'll set it up right now.
54
+ EOS
55
+ Ditz::Config.create_interactively
56
+ end
57
+
58
+ unless op.has_operation? cmd
59
+ die "no such command: #{cmd}"
60
+ end
61
+
62
+ ## talk about the law of unintended consequences. 'gets' requires this.
63
+ args = []
64
+ args << ARGV.shift until ARGV.empty?
65
+
66
+ Ditz::debug "executing command #{cmd}"
67
+ op.do cmd, project, config, *args
68
+
69
+ dirty = project.each_modelobject { |o| break true if o.changed? } || false
70
+ if dirty
71
+ Ditz::debug "project is dirty, saving"
72
+ project.each_modelobject { |o| o.before_serialize project }
73
+ project.save! $opts[:issue_file]
74
+ end
75
+ config.save! $opts[:config_file] if config.changed?
76
+
77
+ # vim: syntax=ruby
@@ -0,0 +1,18 @@
1
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
2
+ <head>
3
+ <title>Component <%= component.name %></title>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
5
+ <link rel="stylesheet" href="style.css" type="text/css" />
6
+ </head>
7
+
8
+ <body>
9
+
10
+ <%= link_to "index", "&laquo; #{project.name} project page" %>
11
+
12
+ <h1><%= project.name %> component: <%= component.name %></h1>
13
+
14
+ <%= render "issue_table", :show_component => false, :show_release => true %>
15
+
16
+ <p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
17
+ </body>
18
+ </html>
@@ -0,0 +1,13 @@
1
+ module Ditz
2
+
3
+ VERSION = "0.1"
4
+
5
+ def debug s
6
+ puts "# #{s}" if $opts[:verbose]
7
+ end
8
+ module_function :debug
9
+
10
+ end
11
+
12
+ require 'model-objects'
13
+ require 'operator'
@@ -0,0 +1,41 @@
1
+ require 'erb'
2
+
3
+ module Ditz
4
+
5
+ ## pass through any variables needed for template generation, and add a bunch
6
+ ## of HTML formatting utility methods.
7
+ class ErbHtml
8
+ def initialize template_dir, template_name, links, mapping={}
9
+ @template_name = template_name
10
+ @template_dir = template_dir
11
+ @links = links
12
+ @mapping = mapping
13
+
14
+ @@erbs ||= {}
15
+ @@erbs[template_name] ||= ERB.new(IO.readlines(File.join(template_dir, "#{template_name}.rhtml")).join)
16
+ end
17
+
18
+ def h o; o.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") end
19
+ def p o; "<p>" + h(o.to_s).gsub("\n\n", "</p><p>") + "</p>" end
20
+ def obscured_email e; h e.gsub(/@.*?(>|$)/, "@...\\1") end
21
+ def link_to o, name
22
+ dest = @links[o]
23
+ dest = o if dest.nil? && o.is_a?(String)
24
+ raise ArgumentError, "no link for #{o.inspect}" unless dest
25
+ "<a href=\"#{dest}\">#{name}</a>"
26
+ end
27
+
28
+ def render template_name, morevars={}
29
+ ErbHtml.new(@template_dir, template_name, @links, @mapping.merge(morevars)).to_s
30
+ end
31
+
32
+ def method_missing meth, *a
33
+ @mapping.member?(meth) ? @mapping[meth] : super
34
+ end
35
+
36
+ def to_s
37
+ @@erbs[@template_name].result binding
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,83 @@
1
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
2
+ <head>
3
+ <title><%= project.name %> Issue Tracker</title>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
5
+ <link rel="stylesheet" href="style.css" type="text/css" />
6
+ </head>
7
+ <body>
8
+
9
+ <h1><%= project.name %> Issue Tracker</h1>
10
+
11
+ <h2>Upcoming Releases</h2>
12
+ <% if upcoming_releases.empty? %>
13
+ <p>No upcoming releases.</p>
14
+ <% else %>
15
+ <ul>
16
+ <% upcoming_releases.each do |r| %>
17
+ <%
18
+ issues = project.issues_for_release r
19
+ num_done = issues.count_of { |i| i.closed? }
20
+ pct_done = issues.size == 0 ? 100 : (100.0 * num_done / issues.size)
21
+ num_bugs_todo = issues.count_of { |i| i.bug? && i.open? }
22
+ num_feats_todo = issues.count_of { |i| i.feature? && i.open? }
23
+ %>
24
+ <li>
25
+ <%= link_to r, "Release #{r.name}" %>:
26
+ <% if issues.empty? %>
27
+ no issues
28
+ <% else %>
29
+ <%= sprintf "%.0f%%", pct_done %> complete;
30
+ <%= "bug".pluralize num_bugs_todo %> and
31
+ <%= "feature".pluralize num_feats_todo %> open.
32
+ <% end %>
33
+ </li>
34
+ <% end %>
35
+ </ul>
36
+ <% end %>
37
+
38
+ <h2>Past Releases</h2>
39
+ <% if past_releases.empty? %>
40
+ <p>No past releases.</p>
41
+ <% else %>
42
+ <ul>
43
+ <% past_releases.each do |r| %>
44
+ <li><%= link_to r, "Release #{r.name}" %>, released <%= r.release_time.pretty_date %>. </li>
45
+ <% end %>
46
+ </ul>
47
+ <% end %>
48
+
49
+ <h2>Unassigned issues</h2>
50
+ <%
51
+ issues = project.unassigned_issues
52
+ open_issues = issues.select { |i| i.open? }
53
+ %>
54
+ <p>
55
+ <%= link_to "unassigned", "unassigned issue".pluralize(issues.size).ucfirst %>; <%= open_issues.size.to_pretty_s %> open.
56
+ </p>
57
+
58
+ <% if components.size > 1 %>
59
+ <h2>Open Issues by component</h2>
60
+ <ul>
61
+ <% components.each do |c| %>
62
+ <%
63
+ open_issues = project.issues_for_component(c).select { |i| i.open? }
64
+ num_bugs_todo = open_issues.count_of { |i| i.bug? }
65
+ num_feats_todo = open_issues.count_of { |i| i.feature? }
66
+ %>
67
+ <li>
68
+ <%= link_to c, c.name %>:
69
+ <% if open_issues.empty? %>
70
+ no open issues.
71
+ <% else %>
72
+ <%= "open bug".pluralize num_bugs_todo %> and
73
+ <%= "open feature".pluralize num_feats_todo %>.
74
+ <% end %>
75
+ </li>
76
+ <% end %>
77
+ </ul>
78
+ <% end %>
79
+
80
+ <p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
81
+
82
+ </body>
83
+ </html>
@@ -0,0 +1,106 @@
1
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
2
+ <head>
3
+ <title><%= issue.title %></title>
4
+ <meta http-equiv="Content-Type" content="text/html; charset=utf8" />
5
+ <link rel="stylesheet" href="style.css" type="text/css" />
6
+ </head>
7
+
8
+ <body>
9
+
10
+ <%= link_to "index", "&laquo; #{project.name} project page" %>
11
+
12
+ <h1><%= issue.title %></h1>
13
+
14
+ <%=
15
+ project.issues.inject(p(issue.desc)) do |s, i|
16
+ s.gsub(/\{issue #{i.id}\}/, link_to(i, i.title))
17
+ end.gsub(/\{issue \w+\}/, "[unknown issue]")
18
+ %>
19
+
20
+ <table>
21
+ <tr>
22
+ <td class="attrname">Id:</td>
23
+ <td class="attrval"><%= issue.id %></td>
24
+ </tr>
25
+
26
+ <tr>
27
+ <td class="attrname">Type:</td>
28
+ <td class="attrval"><%= issue.type %></td>
29
+ </tr>
30
+
31
+ <tr>
32
+ <td class="attrname">Creation time:</td>
33
+ <td class="attrval"><%= issue.creation_time %></td>
34
+ </tr>
35
+
36
+ <tr>
37
+ <td class="attrname">Creator:</td>
38
+ <td class="attrval"><%=obscured_email issue.reporter %></td>
39
+ </tr>
40
+
41
+ <% unless issue.references.empty? %>
42
+ <tr>
43
+ <td class="attrname">References:</td>
44
+ <td class="attrval">
45
+ <% issue.references.each_with_index do |r, i| %>
46
+ [<%= i + 1 %>] <%= link_to r, r %><br/>
47
+ <% end %>
48
+ </td>
49
+ </tr>
50
+
51
+ <% end %>
52
+
53
+ <tr>
54
+ <td class="attrname">Release:</td>
55
+ <td class="attrval">
56
+ <% if release %>
57
+ <%= link_to release, release.name %>
58
+ <% if release.released? %>
59
+ (released <%= release.release_time.pretty_date %>)
60
+ <% else %>
61
+ (unreleased)
62
+ <% end %>
63
+ <% else %>
64
+ <%= link_to "unassigned", "unassigned" %>
65
+ <% end %>
66
+ </td>
67
+ </tr>
68
+
69
+ <tr>
70
+ <td class="attrname">Component:</td>
71
+ <td class="attrval"><%= link_to component, component.name %></td>
72
+ </tr>
73
+
74
+ <tr>
75
+ <td class="attrname">Status:</td>
76
+ <td class="attrval">
77
+ <%= issue.status_string %><% if issue.closed? %>: <%= issue.disposition_string %><% end %>
78
+ </td>
79
+ </tr>
80
+ </table>
81
+
82
+ <h2>Issue log</h2>
83
+
84
+ <table>
85
+ <% issue.log_events.each_with_index do |(time, who, what, comment), i| %>
86
+ <% if i % 2 == 0 %>
87
+ <tr class="logentryeven">
88
+ <% else %>
89
+ <tr class="logentryodd">
90
+ <% end %>
91
+ <td class="logtime"><%=h time %></td>
92
+ <td class="logwho"><%=obscured_email who %></td>
93
+ <td class="logwhat"><%=h what %></td>
94
+ </tr>
95
+ <tr><td colspan="3" class="logcomment">
96
+ <% if comment.empty? %>
97
+ <% else %>
98
+ <%=p comment %>
99
+ <% end %>
100
+ </td></tr>
101
+ <% end %>
102
+ </table>
103
+
104
+ <p class="footer">Generated <%= Time.now %> by <a href="http://ditz.rubyforge.org/">ditz</a>.
105
+ </body>
106
+ </html>