ditz 0.3 → 0.4

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 CHANGED
@@ -1,3 +1,12 @@
1
+ == 0.4 / 2008-07-27
2
+ * bugfix: HOME environment variable now correctly detected on windows
3
+ * hooks loaded from both home directory and project directory
4
+ * added bash shell completion
5
+ * plugin architecture for tighter SCM integration, etc
6
+ * 'ditz grep' should also grep against comments, log messages, etc
7
+ * added man page
8
+ * removed ditz-convert-from-monolith
9
+ * lots of HTML output tweaking
1
10
  == 0.3 / 2008-06-04
2
11
  * readline support for all text entry
3
12
  * hook system. Use ditz -l to see possible hooks.
data/README.txt CHANGED
@@ -7,17 +7,28 @@ http://ditz.rubyforge.org
7
7
  == DESCRIPTION
8
8
 
9
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 directory on disk, with files written in a line-based and human-
12
- editable format. This directory is kept under version control alongside
13
- project code. Changes in issue state is handled by version control like code
14
- change: included as part of a commit, merged with changes from other
15
- developers, conflict-resolved in the standard manner, etc.
10
+ distributed version control systems like git, darcs, Mercurial, and Bazaar. It
11
+ can also be used with centralized systems like SVN.
12
+
13
+ Ditz maintains an issue database directory on disk, with files written in a
14
+ line-based and human-editable format. This directory can be kept under version
15
+ control, alongside project code.
16
+
17
+ There are different ways to use ditz:
18
+
19
+ 1. Treat issue change the same as code change: include it as part of commits,
20
+ and merge it with changes from other developers. (Resolving conflicts in
21
+ the usual manner.)
22
+ 2. Keep the issue database in the repository but in a separate branch. Issue
23
+ changes can be managed by your VCS, but is not tied directly to commits.
24
+ 3. Keep the issue database separate and not under VCS at all.
25
+
26
+ Your particular usage will depend on what you want to get out of ditz.
16
27
 
17
28
  Ditz provides a simple, console-based interface for creating and updating the
18
- issue database files, and some rudimentary HTML generation capabilities for
19
- producing world-readable status pages. It offers no central public method of
20
- bug submission.
29
+ issue database file, and some rudimentary HTML generation capabilities for
30
+ producing world-readable status pages. It currently offers no central public
31
+ method of bug submission.
21
32
 
22
33
  == SYNOPSIS
23
34
 
@@ -30,7 +41,7 @@ bug submission.
30
41
 
31
42
  # where am i?
32
43
  4. ditz status
33
- 5. ditz todo
44
+ 5. ditz todo (or simply "ditz")
34
45
 
35
46
  # do work
36
47
  6. write code
@@ -94,7 +105,7 @@ http://ditz.rubyforge.org/ditz/issue-0704dafe4aef96279364013aba177a0971d425cb.ht
94
105
 
95
106
  == REQUIREMENTS
96
107
 
97
- * trollop >= 1.7
108
+ * trollop >= 1.8.2
98
109
 
99
110
  == INSTALLATION
100
111
 
data/Rakefile CHANGED
@@ -17,29 +17,15 @@ Hoe.new('ditz', Ditz::VERSION) do |p|
17
17
  p.url = "http://ditz.rubyforge.org"
18
18
  p.changes = p.paragraphs_of('Changelog', 0..0).join("\n\n")
19
19
  p.email = "wmorgan-ditz@masanjin.net"
20
- p.extra_deps = [['trollop', '>= 1.7']]
20
+ p.extra_deps = [['trollop', '>= 1.8.2']]
21
21
  end
22
22
 
23
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
24
 
35
25
  task :upload_webpage => WWW_FILES do |t|
36
26
  sh "rsync -essh -cavz #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
37
27
  end
38
28
 
39
- task :upload_webpage_images => (SCREENSHOTS + SCREENSHOTS_SMALL) do |t|
40
- sh "rsync -essh -cavs #{t.prerequisites * ' '} wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
41
- end
42
-
43
29
  task :upload_report do |t|
44
30
  sh "ruby -Ilib bin/ditz html ditz"
45
31
  sh "rsync -essh -cavz ditz wmorgan@rubyforge.org:/var/www/gforge-projects/ditz/"
@@ -1,3 +1,32 @@
1
+ 0.4
2
+ ---
3
+ - Command-line completion scripts are now included for bash and zsh. To
4
+ activate, source the relevant file in $GEMDIR/ditz-0.4/contrib/completion/.
5
+
6
+ - Hooks can now be set on a per-project basis. Make a .ditz/hooks directory in
7
+ your project root and place them there. These will be loaded after any
8
+ hooks in ~/.ditz/hooks, so they can override or simply supplement.
9
+
10
+ - The plugin system is done. There's currently one plugin, for git integration.
11
+ To enable it, add the line "- git" in a .ditz-plugins file in your project
12
+ root. The git plugin currently has the following features:
13
+
14
+ - Issues can have a git branch assigned to them with "ditz set-branch".
15
+ - Git commit messages can have a Ditz-issue: header auto-filled if you
16
+ commit with "ditz commit <issue>" (i.e. instead of git commit).
17
+ - In both HTML and screen output, commits from the assigned branch, and
18
+ commits with the corresponding Ditz-issue: header in the log message,
19
+ will be listed for each issue.
20
+
21
+ Note that the plugin system is independent of the hook system. In order
22
+ to auto-add ditz files to the git index upon modification, you must set
23
+ up hooks. Example hooks for git are at:
24
+ http://hackety.org/2008/06/26/gitHooksForDitz.html
25
+
26
+ Also note that as soon as a feature branch is merged back into master, ditz
27
+ loses the ability to distinguish its commits. So the Ditz-issue: approach
28
+ is probably better if you want a long-term record.
29
+
1
30
  0.3
2
31
  ---
3
32
  Ditz now works from project subdirectories, and you can have a .ditz-config in
data/bin/ditz CHANGED
@@ -19,19 +19,16 @@ require "ditz"
19
19
 
20
20
  PROJECT_FN = "project.yaml"
21
21
  CONFIG_FN = ".ditz-config"
22
+ PLUGIN_FN = ".ditz-plugins"
22
23
 
23
- ## helper for recursive search
24
- def find_dir_containing target, start=Pathname.new(".")
25
- return start if (start + target).exist?
26
- unless start.parent.realpath == start.realpath
27
- find_dir_containing target, start.parent
28
- end
29
- end
24
+ config_dir = Ditz::find_dir_containing CONFIG_FN
25
+ plugin_dir = Ditz::find_dir_containing PLUGIN_FN
30
26
 
31
27
  $opts = options do
32
28
  version "ditz #{Ditz::VERSION}"
33
29
  opt :issue_dir, "Issue database dir", :default => "bugs"
34
- opt :config_file, "Configuration file", :default => File.join(ENV["HOME"], CONFIG_FN)
30
+ opt :config_file, "Configuration file", :default => File.join(config_dir || ".", CONFIG_FN)
31
+ opt :plugins_file, "Plugins file", :default => File.join(plugin_dir || ".", PLUGIN_FN)
35
32
  opt :verbose, "Verbose output", :default => false
36
33
  opt :no_comment, "Skip asking for a comment", :default => false
37
34
  opt :list_hooks, "List all hooks and descriptions, and quit.", :short => 'l', :default => false
@@ -76,22 +73,36 @@ if $opts[:list_hooks]
76
73
  exit 0
77
74
  end
78
75
 
79
- local_config_dir = find_dir_containing CONFIG_FN
76
+ plugins = begin
77
+ Ditz::debug "loading plugins from #{$opts[:plugins_file]}"
78
+ YAML::load_file$opts[:plugins_file]
79
+ rescue SystemCallError => e
80
+ Ditz::debug "can't load plugins file: #{e.message}"
81
+ []
82
+ end
83
+
84
+ plugins.each do |p|
85
+ fn = Ditz::find_ditz_file "plugins/#{p}.rb"
86
+ Ditz::debug "loading plugin #{p.inspect} from #{fn}"
87
+ load fn
88
+ end
89
+
80
90
  config = begin
81
- if local_config_dir
82
- fn = local_config_dir + CONFIG_FN
83
- Ditz::debug "loading local config from #{fn}"
84
- Ditz::Config.from fn
91
+ Ditz::debug "loading config from #{$opts[:config_file]}"
92
+ Ditz::Config.from $opts[:config_file]
93
+ rescue SystemCallError => e
94
+ if ARGV.member? "<options>"
95
+ ## special case here. if we're asking for tab completion, and the config
96
+ ## file doesn't exist, don't do the interactive building. just make a
97
+ ## fake empty one and carry on.
98
+ Ditz::Config.new
85
99
  else
86
- Ditz::debug "loading global config from #{$opts[:config_file]}"
87
- Ditz::Config.from $opts[:config_file]
88
- end
89
- rescue SystemCallError, Ditz::ModelObject::ModelError => e
90
- puts <<EOS
100
+ puts <<EOS
91
101
  I wasn't able to find a configuration file #{$opts[:config_file]}.
92
102
  We'll set it up right now.
93
103
  EOS
94
- Ditz::Config.create_interactively.save! $opts[:config_file]
104
+ Ditz::Config.create_interactively.save! $opts[:config_file]
105
+ end
95
106
  end
96
107
 
97
108
  cmd = ARGV.shift || "todo"
@@ -111,7 +122,7 @@ when "help"
111
122
  exit
112
123
  end
113
124
 
114
- project_root = find_dir_containing(issue_dir + PROJECT_FN)
125
+ project_root = Ditz::find_dir_containing(issue_dir + PROJECT_FN)
115
126
  die "No #{issue_dir} directory---use 'ditz init' to initialize" unless project_root
116
127
  project_root += issue_dir
117
128
 
@@ -149,6 +160,7 @@ begin
149
160
  rescue Ditz::Operator::Error => e
150
161
  die e.message
151
162
  rescue Errno::EPIPE, Interrupt
163
+ puts
152
164
  exit 1
153
165
  end
154
166
 
@@ -0,0 +1,29 @@
1
+ #compdef ditz
2
+
3
+ ME=ditz
4
+ COMMANDS=--commands
5
+ OPTIONS='<options>'
6
+
7
+ if (($CURRENT == 2)); then
8
+ # We're completing the first word after the tool: the command.
9
+ _wanted command expl "$ME command" \
10
+ compadd -- $( "$ME" "$COMMANDS" )
11
+ else
12
+ # Find the options/files/URL/etc. for the current command by using the tool itself.
13
+ case "${words[$CURRENT]}"; in
14
+ -*)
15
+ _wanted args expl "Arguments for $ME ${words[2]}" \
16
+ compadd -- $( "$ME" "${words[2]}" "$OPTIONS" ; _files )
17
+ ;;
18
+ ht*|ft*)
19
+ _arguments '*:URL:_urls'
20
+ ;;
21
+ /*|./*|\~*|../*)
22
+ _arguments '*:file:_files'
23
+ ;;
24
+ *)
25
+ _wanted args expl "Arguments for $ME ${words[2]}" \
26
+ compadd -- $( "$ME" "${words[2]}" "$OPTIONS" )
27
+ ;;
28
+ esac
29
+ fi
@@ -0,0 +1,22 @@
1
+ # ditz bash completion
2
+ #
3
+ # author: Christian Garbs
4
+ #
5
+ # based on bzr.simple by Martin Pool
6
+
7
+ _ditz()
8
+ {
9
+ cur=${COMP_WORDS[COMP_CWORD]}
10
+ if [ $COMP_CWORD -eq 1 ]; then
11
+ COMPREPLY=( $( compgen -W "$(ditz --commands)" $cur ) )
12
+ elif [ $COMP_CWORD -eq 2 ]; then
13
+ cmd=${COMP_WORDS[1]}
14
+ COMPREPLY=( $( compgen -W "$(ditz "$cmd" '<options>' 2>/dev/null)" $cur ) )
15
+ elif [ $COMP_CWORD -eq 3 ]; then
16
+ cmd=${COMP_WORDS[1]}
17
+ parm1=${COMP_WORDS[2]}
18
+ COMPREPLY=( $( compgen -W "$(ditz "$cmd" "$parm1" '<options>' 2>/dev/null)" $cur ) )
19
+ fi
20
+ }
21
+
22
+ complete -F _ditz -o default ditz
@@ -1,6 +1,6 @@
1
1
  module Ditz
2
2
 
3
- VERSION = "0.3"
3
+ VERSION = "0.4"
4
4
 
5
5
  def debug s
6
6
  puts "# #{s}" if $opts[:verbose]
@@ -14,7 +14,6 @@ end
14
14
  def self.has_readline= val
15
15
  @has_readline = val
16
16
  end
17
- end
18
17
 
19
18
  begin
20
19
  Ditz::has_readline = false
@@ -24,6 +23,34 @@ rescue LoadError
24
23
  # do nothing
25
24
  end
26
25
 
26
+ def home_dir
27
+ @home ||=
28
+ ENV["HOME"] || (ENV["HOMEDRIVE"] && ENV["HOMEPATH"] ? ENV["HOMEDRIVE"] + ENV["HOMEPATH"] : nil) || begin
29
+ $stderr.puts "warning: can't determine home directory, using '.'"
30
+ "."
31
+ end
32
+ end
33
+
34
+ ## helper for recursive search
35
+ def find_dir_containing target, start=Pathname.new(".")
36
+ return start if (start + target).exist?
37
+ unless start.parent.realpath == start.realpath
38
+ find_dir_containing target, start.parent
39
+ end
40
+ end
41
+
42
+ ## my brilliant solution to the 'gem datadir' problem
43
+ def find_ditz_file fn
44
+ dir = $:.find { |p| File.exist? File.expand_path(File.join(p, fn)) }
45
+ raise "can't find #{fn} in any load path" unless dir
46
+ File.expand_path File.join(dir, fn)
47
+ end
48
+
49
+ module_function :home_dir, :find_dir_containing, :find_ditz_file
50
+ end
51
+
27
52
  require 'model-objects'
28
53
  require 'operator'
54
+ require 'views'
29
55
  require 'hook'
56
+
@@ -50,8 +50,15 @@ EOS
50
50
 
51
51
  def hooks_for name
52
52
  if @blocks[name].nil? || @blocks[name].empty?
53
- fns = File.join(ENV['HOME'], '.ditz', 'hooks', '*.rb')
54
- Dir[fns].each { |fn| load fn }
53
+ dirs = [Ditz::home_dir, Ditz::find_dir_containing(".ditz")].compact.map do |d|
54
+ File.join d, ".ditz", "hooks"
55
+ end
56
+ Ditz::debug "looking for hooks in #{dirs.join(" and ")}"
57
+ files = dirs.map { |d| Dir[File.join(d, "*.rb")] }.flatten
58
+ files.each do |fn|
59
+ Ditz::debug "loading hook file #{fn}"
60
+ load fn
61
+ end
55
62
  end
56
63
 
57
64
  @blocks[name] || []
@@ -5,17 +5,41 @@ module Ditz
5
5
  ## pass through any variables needed for template generation, and add a bunch
6
6
  ## of HTML formatting utility methods.
7
7
  class ErbHtml
8
- def initialize template_dir, template_name, links, mapping={}
9
- @template_name = template_name
8
+ def initialize template_dir, links, binding={}
10
9
  @template_dir = template_dir
11
10
  @links = links
12
- @mapping = mapping
11
+ @binding = binding
12
+ end
13
+
14
+ ## return an ErbHtml object that has the current binding plus extra_binding merged in
15
+ def clone_for_binding extra_binding={}
16
+ extra_binding.empty? ? self : ErbHtml.new(@template_dir, @links, @binding.merge(extra_binding))
17
+ end
18
+
19
+ def render_template template_name, extra_binding={}
20
+ if extra_binding.empty?
21
+ @@erbs ||= {}
22
+ @@erbs[template_name] ||= ERB.new IO.read(File.join(@template_dir, "#{template_name}.rhtml"))
23
+ @@erbs[template_name].result binding
24
+ else
25
+ clone_for_binding(extra_binding).render_template template_name
26
+ end
27
+ end
13
28
 
14
- @@erbs ||= {}
15
- @@erbs[template_name] ||= ERB.new(IO.readlines(File.join(template_dir, "#{template_name}.rhtml")).join)
29
+ def render_string s, extra_binding={}
30
+ if extra_binding.empty?
31
+ ERB.new(s).result binding
32
+ else
33
+ clone_for_binding(extra_binding).render_string s
34
+ end
16
35
  end
17
36
 
37
+ ###
38
+ ### the following methods are meant to be called from the ERB itself
39
+ ###
40
+
18
41
  def h o; o.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;") end
42
+ def t o; o.strftime "%Y-%m-%d %H:%M %Z" end
19
43
  def p o; "<p>" + h(o.to_s).gsub("\n\n", "</p><p>") + "</p>" end
20
44
  def obscured_email e; h e.gsub(/@.*?(>|$)/, "@...\\1") end
21
45
  def link_to o, name
@@ -24,23 +48,21 @@ class ErbHtml
24
48
  raise ArgumentError, "no link for #{o.inspect}" unless dest
25
49
  "<a href=\"#{dest}\">#{name}</a>"
26
50
  end
51
+ def fancy_issue_link_for i
52
+ "<span class=\"issuestatus_#{i.status}\">" + link_to(i, "[#{i.title}]") + "</span>"
53
+ end
27
54
 
28
55
  def link_issue_names project, s
29
56
  project.issues.inject(s) do |s, i|
30
- s.gsub(/\b#{i.name}\b/, link_to(i, i.title))
57
+ s.gsub(/\b#{i.name}\b/, fancy_issue_link_for(i))
31
58
  end
32
59
  end
33
60
 
34
- def render template_name, morevars={}
35
- ErbHtml.new(@template_dir, template_name, @links, @mapping.merge(morevars)).to_s
36
- end
61
+ ## render a nested ERB
62
+ alias :render :render_template
37
63
 
38
64
  def method_missing meth, *a
39
- @mapping.member?(meth) ? @mapping[meth] : super
40
- end
41
-
42
- def to_s
43
- @@erbs[@template_name].result binding
65
+ @binding.member?(meth) ? @binding[meth] : super
44
66
  end
45
67
  end
46
68
 
@@ -69,10 +69,13 @@
69
69
  open_issues = project.group_issues(project.issues_for_component(c).select { |i| i.open? })
70
70
  %>
71
71
  <li>
72
- <%= link_to c, c.name %>:
73
72
  <% if open_issues.empty? %>
73
+ <span class="dimmed">
74
+ <%= link_to c, c.name %>:
74
75
  no open issues.
76
+ </span>
75
77
  <% else %>
78
+ <%= link_to c, c.name %>:
76
79
  <%= open_issues.map { |n,is| n.to_s.pluralize is.size }.join(' and ') %> open.
77
80
  <% end %>
78
81
  </li>
@@ -80,6 +83,26 @@
80
83
  </ul>
81
84
  <% end %>
82
85
 
86
+ <h2>Recent activity</h2>
87
+
88
+ <table>
89
+ <% project.issues.map { |i| i.log_events.map { |e| [e, i] } }.
90
+ flatten_one_level.
91
+ sort_by { |e| e.first.first }.
92
+ reverse[0 ... 10].
93
+ each do |(date, who, what, comment), i| %>
94
+ <tr>
95
+ <td><%= date.pretty_date %></td>
96
+ <td class="issuename">
97
+ <%= link_issue_names project, h(i.name) %>
98
+ <%= what %> by <%= who.shortened_email %></td>
99
+ </tr>
100
+ <% if comment && comment =~ /\S/ %>
101
+ <tr><td></td><td><i><%= link_issue_names project, h(comment) %></i></td></tr>
102
+ <% end %>
103
+ <% end %>
104
+ </table>
105
+
83
106
  <p class="footer">Generated by <a href="http://ditz.rubyforge.org/">ditz</a>.
84
107
 
85
108
  </body>