ditz 0.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
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>