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/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
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
19
|
-
producing world-readable status pages. It offers no central public
|
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.
|
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.
|
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/"
|
data/ReleaseNotes
CHANGED
@@ -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
|
-
|
24
|
-
|
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(
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/ditz.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Ditz
|
2
2
|
|
3
|
-
VERSION = "0.
|
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
|
+
|
data/lib/hook.rb
CHANGED
@@ -50,8 +50,15 @@ EOS
|
|
50
50
|
|
51
51
|
def hooks_for name
|
52
52
|
if @blocks[name].nil? || @blocks[name].empty?
|
53
|
-
|
54
|
-
|
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] || []
|
data/lib/html.rb
CHANGED
@@ -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,
|
9
|
-
@template_name = template_name
|
8
|
+
def initialize template_dir, links, binding={}
|
10
9
|
@template_dir = template_dir
|
11
10
|
@links = links
|
12
|
-
@
|
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
|
-
|
15
|
-
|
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("&", "&").gsub("<", "<").gsub(">", ">") 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/,
|
57
|
+
s.gsub(/\b#{i.name}\b/, fancy_issue_link_for(i))
|
31
58
|
end
|
32
59
|
end
|
33
60
|
|
34
|
-
|
35
|
-
|
36
|
-
end
|
61
|
+
## render a nested ERB
|
62
|
+
alias :render :render_template
|
37
63
|
|
38
64
|
def method_missing meth, *a
|
39
|
-
@
|
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
|
|
data/lib/index.rhtml
CHANGED
@@ -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>
|