sup 0.19.0 → 0.23

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.
Files changed (93) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +4 -1
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +12 -6
  5. data/CONTRIBUTORS +28 -14
  6. data/Gemfile +5 -0
  7. data/History.txt +92 -0
  8. data/README.md +26 -5
  9. data/Rakefile +41 -1
  10. data/ReleaseNotes +17 -0
  11. data/bin/sup +12 -23
  12. data/bin/sup-add +15 -16
  13. data/bin/sup-config +30 -45
  14. data/bin/sup-dump +2 -3
  15. data/bin/sup-import-dump +5 -6
  16. data/bin/sup-sync +3 -4
  17. data/bin/sup-sync-back-maildir +3 -4
  18. data/bin/sup-tweak-labels +6 -7
  19. data/contrib/colorpicker.rb +0 -2
  20. data/contrib/completion/_sup.bash +102 -0
  21. data/devel/profile.rb +0 -1
  22. data/ext/mkrf_conf_xapian.rb +47 -0
  23. data/lib/sup.rb +10 -8
  24. data/lib/sup/buffer.rb +12 -0
  25. data/lib/sup/colormap.rb +5 -2
  26. data/lib/sup/contact.rb +4 -2
  27. data/lib/sup/crypto.rb +58 -16
  28. data/lib/sup/draft.rb +8 -8
  29. data/lib/sup/hook.rb +9 -9
  30. data/lib/sup/index.rb +20 -7
  31. data/lib/sup/label.rb +1 -1
  32. data/lib/sup/logger.rb +1 -1
  33. data/lib/sup/maildir.rb +16 -5
  34. data/lib/sup/mbox.rb +13 -5
  35. data/lib/sup/message.rb +36 -12
  36. data/lib/sup/message_chunks.rb +13 -4
  37. data/lib/sup/mode.rb +34 -28
  38. data/lib/sup/modes/contact_list_mode.rb +1 -0
  39. data/lib/sup/modes/edit_message_mode.rb +3 -2
  40. data/lib/sup/modes/forward_mode.rb +22 -3
  41. data/lib/sup/modes/line_cursor_mode.rb +1 -1
  42. data/lib/sup/modes/reply_mode.rb +3 -1
  43. data/lib/sup/modes/text_mode.rb +6 -1
  44. data/lib/sup/modes/thread_index_mode.rb +12 -2
  45. data/lib/sup/modes/thread_view_mode.rb +111 -14
  46. data/lib/sup/person.rb +68 -61
  47. data/lib/sup/search.rb +1 -1
  48. data/lib/sup/sent.rb +1 -1
  49. data/lib/sup/source.rb +1 -1
  50. data/lib/sup/util.rb +15 -94
  51. data/lib/sup/util/axe.rb +17 -0
  52. data/lib/sup/util/locale_fiddler.rb +24 -0
  53. data/lib/sup/util/ncurses.rb +3 -3
  54. data/lib/sup/version.rb +10 -1
  55. data/sup.gemspec +29 -11
  56. data/test/{messages → fixtures}/bad-content-transfer-encoding-1.eml +0 -0
  57. data/test/{messages → fixtures}/binary-content-transfer-encoding-2.eml +0 -0
  58. data/test/fixtures/blank-header-fields.eml +71 -0
  59. data/test/fixtures/contacts.txt +1 -0
  60. data/test/fixtures/mailing-list-header.eml +80 -0
  61. data/test/fixtures/malicious-attachment-names.eml +55 -0
  62. data/test/fixtures/missing-from-to.eml +18 -0
  63. data/test/{messages → fixtures}/missing-line.eml +0 -0
  64. data/test/fixtures/multi-part-2.eml +72 -0
  65. data/test/fixtures/multi-part.eml +61 -0
  66. data/test/fixtures/no-body.eml +18 -0
  67. data/test/fixtures/simple-message.eml +29 -0
  68. data/test/fixtures/text-attachments-with-charset.eml +46 -0
  69. data/test/fixtures/zimbra-quote-with-bottom-post.eml +27 -0
  70. data/test/gnupg_test_home/gpg.conf +3 -1
  71. data/test/gnupg_test_home/private-keys-v1.d/306D2EE90FF0014B5B9FD07E265C751791674140.key +0 -0
  72. data/test/gnupg_test_home/pubring.gpg +0 -0
  73. data/test/gnupg_test_home/receiver_pubring.gpg +0 -0
  74. data/test/gnupg_test_home/receiver_secring.gpg +0 -0
  75. data/test/gnupg_test_home/regen_keys.sh +89 -0
  76. data/test/gnupg_test_home/secring.gpg +0 -0
  77. data/test/gnupg_test_home/sup-test-2@foo.bar.asc +20 -17
  78. data/test/integration/test_maildir.rb +75 -0
  79. data/test/integration/test_mbox.rb +69 -0
  80. data/test/test_crypto.rb +14 -2
  81. data/test/test_header_parsing.rb +1 -1
  82. data/test/test_helper.rb +6 -3
  83. data/test/test_message.rb +115 -341
  84. data/test/test_messages_dir.rb +4 -28
  85. data/test/test_yaml_regressions.rb +1 -1
  86. data/test/unit/test_contact.rb +33 -0
  87. data/test/unit/test_locale_fiddler.rb +15 -0
  88. data/test/unit/test_person.rb +37 -0
  89. data/test/unit/util/test_query.rb +10 -4
  90. data/test/unit/util/test_string.rb +6 -0
  91. metadata +137 -53
  92. data/test/gnupg_test_home/receiver_trustdb.gpg +0 -0
  93. data/test/gnupg_test_home/trustdb.gpg +0 -0
@@ -1,4 +1,3 @@
1
- require 'rubygems'
2
1
  require 'ruby-prof'
3
2
  require "redwood"
4
3
 
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'rubygems/command.rb'
3
+ require 'rubygems/dependency_installer.rb'
4
+ require 'rbconfig'
5
+
6
+ begin
7
+ Gem::Command.build_args = ARGV
8
+ rescue NoMethodError
9
+ end
10
+
11
+ puts "xapian: platform specific dependencies.."
12
+
13
+ inst = Gem::DependencyInstaller.new
14
+ begin
15
+
16
+ if !RbConfig::CONFIG['arch'].include?('openbsd')
17
+ # update version in Gemfile as well
18
+ name = "xapian-ruby"
19
+ version = "~> 1.2"
20
+
21
+ begin
22
+ # try to load gem
23
+
24
+ gem name, version
25
+ STDERR.puts "xapian: already installed."
26
+
27
+ rescue Gem::LoadError
28
+
29
+ STDERR.puts "xapian: installing xapian-ruby.."
30
+ inst.install name, version
31
+
32
+ end
33
+ else
34
+ STDERR.puts "xapian: openbsd: you have to install xapian-core and xapian-bindings manually, have a look at: https://github.com/sup-heliotrope/sup/wiki/Installation%3A-OpenBSD"
35
+ end
36
+
37
+ rescue
38
+
39
+ exit(1)
40
+
41
+ end
42
+
43
+ # create dummy rakefile to indicate success
44
+ f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w")
45
+ f.write("task :default\n")
46
+ f.close
47
+
data/lib/sup.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'rubygems'
4
3
  require 'yaml'
5
4
  require 'zlib'
6
5
  require 'thread'
@@ -8,6 +7,7 @@ require 'fileutils'
8
7
  require 'locale'
9
8
  require 'ncursesw'
10
9
  require 'rmail'
10
+ require 'uri'
11
11
  begin
12
12
  require 'fastthread'
13
13
  rescue LoadError
@@ -64,6 +64,7 @@ module Redwood
64
64
  LEGACY_YAML_DOMAIN = "masanjin.net"
65
65
  YAML_DATE = "2006-10-01"
66
66
  MAILDIR_SYNC_CHECK_SKIPPED = 'SKIPPED'
67
+ URI_ENCODE_CHARS = "!*'();:@&=+$,?#[] " # see https://en.wikipedia.org/wiki/Percent-encoding
67
68
 
68
69
  ## record exceptions thrown in threads nicely
69
70
  @exceptions = []
@@ -103,7 +104,7 @@ module Redwood
103
104
  o
104
105
  end
105
106
 
106
- mode = if File.exists? fn
107
+ mode = if File.exist? fn
107
108
  File.stat(fn).mode
108
109
  else
109
110
  0600
@@ -111,7 +112,7 @@ module Redwood
111
112
 
112
113
  if backup
113
114
  backup_fn = fn + '.bak'
114
- if File.exists?(fn) && File.size(fn) > 0
115
+ if File.exist?(fn) && File.size(fn) > 0
115
116
  File.open(backup_fn, "w", mode) do |f|
116
117
  File.open(fn, "r") { |old_f| FileUtils.copy_stream old_f, f }
117
118
  f.fsync
@@ -137,7 +138,7 @@ module Redwood
137
138
  end
138
139
 
139
140
  def load_yaml_obj fn, compress=false
140
- o = if File.exists? fn
141
+ o = if File.exist? fn
141
142
  if compress
142
143
  Zlib::GzipReader.open(fn) { |f| YAML::load f }
143
144
  else
@@ -178,7 +179,7 @@ module Redwood
178
179
  return if bypass_sync_check
179
180
 
180
181
  if $config[:sync_back_to_maildir]
181
- if not File.exists? Redwood::SYNC_OK_FN
182
+ if not File.exist? Redwood::SYNC_OK_FN
182
183
  Redwood.warn_syncback <<EOS
183
184
  It appears that the "sync_back_to_maildir" option has been changed
184
185
  from false to true since the last execution of sup.
@@ -189,14 +190,14 @@ Should I complain about this again? (Y/n)
189
190
  EOS
190
191
  File.open(Redwood::SYNC_OK_FN, 'w') {|f| f.write(Redwood::MAILDIR_SYNC_CHECK_SKIPPED) } if STDIN.gets.chomp.downcase == 'n'
191
192
  end
192
- elsif not $config[:sync_back_to_maildir] and File.exists? Redwood::SYNC_OK_FN
193
+ elsif not $config[:sync_back_to_maildir] and File.exist? Redwood::SYNC_OK_FN
193
194
  File.delete(Redwood::SYNC_OK_FN)
194
195
  end
195
196
  end
196
197
 
197
198
  def check_syncback_settings
198
199
  # don't check if syncback was never performed
199
- return unless File.exists? Redwood::SYNC_OK_FN
200
+ return unless File.exist? Redwood::SYNC_OK_FN
200
201
  active_sync_sources = File.readlines(Redwood::SYNC_OK_FN).collect { |e| e.strip }.find_all { |e| not e.empty? }
201
202
  return if active_sync_sources.length == 1 and active_sync_sources[0] == Redwood::MAILDIR_SYNC_CHECK_SKIPPED
202
203
  sources = SourceManager.sources
@@ -330,13 +331,14 @@ EOM
330
331
  :poll_interval => 300,
331
332
  :wrap_width => 0,
332
333
  :slip_rows => 0,
334
+ :indent_spaces => 2,
333
335
  :col_jump => 2,
334
336
  :stem_language => "english",
335
337
  :sync_back_to_maildir => false,
336
338
  :continuous_scroll => false,
337
339
  :always_edit_async => false,
338
340
  }
339
- if File.exists? filename
341
+ if File.exist? filename
340
342
  config = Redwood::load_yaml_obj filename
341
343
  abort "#{filename} is not a valid configuration file (it's a #{config.class}, not a hash)" unless config.is_a?(Hash)
342
344
  default_config.merge config
@@ -396,6 +396,18 @@ EOS
396
396
  end
397
397
  end
398
398
 
399
+ ## ask* functions. these functions display a one-line text field with
400
+ ## a prompt at the bottom of the screen. answers typed or choosen by
401
+ ## tab-completion
402
+ ##
403
+ ## common arguments are:
404
+ ##
405
+ ## domain: token used as key for @textfields, which seems to be a
406
+ ## dictionary of input field objects
407
+ ## question: string used as prompt
408
+ ## completions: array of possible answers, that can be completed by using
409
+ ## the tab key
410
+ ## default: default value to return
399
411
  def ask_with_completions domain, question, completions, default=nil
400
412
  ask domain, question, default do |s|
401
413
  s.fix_encoding!
@@ -17,6 +17,9 @@ module Ncurses
17
17
 
18
18
  ## xterm 24-shade grayscale
19
19
  24.times { |x| color! "g#{x}", (16+6*6*6) + x }
20
+ elsif Ncurses::NUM_COLORS == -1
21
+ ## Terminal emulator doesn't appear to support colors
22
+ fail "sup must be run in a terminal with color support, please check your TERM variable."
20
23
  end
21
24
  end
22
25
 
@@ -186,13 +189,13 @@ class Colormap
186
189
  ## Try to use the user defined colors, in case of an error fall back
187
190
  ## to the default ones.
188
191
  def populate_colormap
189
- user_colors = if File.exists? Redwood::COLOR_FN
192
+ user_colors = if File.exist? Redwood::COLOR_FN
190
193
  debug "loading user colors from #{Redwood::COLOR_FN}"
191
194
  Redwood::load_yaml_obj Redwood::COLOR_FN
192
195
  end
193
196
 
194
197
  ## Set attachment sybmol to sane default for existing colorschemes
195
- if user_colors and user_colors.has_key? :to_me
198
+ if user_colors and user_colors.has_key? :to_me
196
199
  user_colors[:with_attachment] = user_colors[:to_me] unless user_colors.has_key? :with_attachment
197
200
  end
198
201
 
@@ -16,7 +16,7 @@ class ContactManager
16
16
  @a2p = {} # alias to person
17
17
  @e2p = {} # email to person
18
18
 
19
- if File.exists? fn
19
+ if File.exist? fn
20
20
  IO.foreach(fn) do |l|
21
21
  l =~ /^([^:]*): (.*)$/ or raise "can't parse #{fn} line #{l.inspect}"
22
22
  aalias, addr = $1, $2
@@ -29,11 +29,13 @@ class ContactManager
29
29
  def contacts_with_aliases; @a2p.values.uniq end
30
30
 
31
31
  def update_alias person, aalias=nil
32
+ ## Deleting old data if it exists
32
33
  old_aalias = @p2a[person]
33
- if(old_aalias != nil and old_aalias != "") # remove old alias
34
+ if old_aalias
34
35
  @a2p.delete old_aalias
35
36
  @e2p.delete person.email
36
37
  end
38
+ ## Update with new data
37
39
  @p2a[person] = aalias
38
40
  unless aalias.nil? || aalias.empty?
39
41
  @a2p[aalias] = person
@@ -10,11 +10,14 @@ class CryptoManager
10
10
 
11
11
  class Error < StandardError; end
12
12
 
13
- OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
14
- [:sign, "Sign"],
15
- [:sign_and_encrypt, "Sign and encrypt"],
16
- [:encrypt, "Encrypt only"]
17
- )
13
+ OUTGOING_MESSAGE_OPERATIONS = {
14
+ sign: "Sign",
15
+ sign_and_encrypt: "Sign and encrypt",
16
+ encrypt: "Encrypt only"
17
+ }
18
+
19
+ KEY_PATTERN = /(-----BEGIN PGP PUBLIC KEY BLOCK.*-----END PGP PUBLIC KEY BLOCK)/m
20
+ KEYSERVER_URL = "http://pool.sks-keyservers.net:11371/pks/lookup"
18
21
 
19
22
  HookManager.register "gpg-options", <<EOS
20
23
  Runs before gpg is called, allowing you to modify the options (most
@@ -124,18 +127,27 @@ EOS
124
127
  def sign from, to, payload
125
128
  return unknown_status(@not_working_reason) unless @not_working_reason.nil?
126
129
 
130
+ # We grab this from the GPG::Ctx below after signing, so that we can set
131
+ # micalg in Content-Type to match the hash algorithm GPG decided to use.
132
+ hash_algo = nil
133
+
127
134
  gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true}
128
135
  gpg_opts.merge!(gen_sign_user_opts(from))
129
136
  gpg_opts = HookManager.run("gpg-options",
130
137
  {:operation => "sign", :options => gpg_opts}) || gpg_opts
131
138
  begin
132
- if GPGME.respond_to?('detach_sign')
133
- sig = GPGME.detach_sign(format_payload(payload), gpg_opts)
134
- else
135
- crypto = GPGME::Crypto.new
136
- gpg_opts[:mode] = GPGME::SIG_MODE_DETACH
137
- sig = crypto.sign(format_payload(payload), gpg_opts).read
139
+ input = GPGME::Data.new(format_payload(payload))
140
+ output = GPGME::Data.new()
141
+ GPGME::Ctx.new(gpg_opts) do |ctx|
142
+ if gpg_opts[:signer]
143
+ signers = GPGME::Key.find(:secret, gpg_opts[:signer], :sign)
144
+ ctx.add_signer(*signers)
145
+ end
146
+ ctx.sign(input, output, GPGME::SIG_MODE_DETACH)
147
+ hash_algo = GPGME::hash_algo_name(ctx.sign_result.signatures[0].hash_algo)
138
148
  end
149
+ output.seek(0)
150
+ sig = output.read
139
151
  rescue GPGME::Error => exc
140
152
  raise Error, gpgme_exc_msg(exc.message)
141
153
  end
@@ -147,7 +159,7 @@ EOS
147
159
  end
148
160
 
149
161
  envelope = RMail::Message.new
150
- envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature'
162
+ envelope.header["Content-Type"] = "multipart/signed; protocol=application/pgp-signature; micalg=pgp-#{hash_algo.downcase}"
151
163
 
152
164
  envelope.add_part payload
153
165
  signature = RMail::Message.make_attachment sig, "application/pgp-signature", nil, "signature.asc"
@@ -212,9 +224,10 @@ EOS
212
224
  unknown = false
213
225
  all_output_lines = []
214
226
  all_trusted = true
227
+ unknown_fingerprint = nil
215
228
 
216
229
  verify_result.signatures.each do |signature|
217
- output_lines, trusted = sig_output_lines signature
230
+ output_lines, trusted, unknown_fingerprint = sig_output_lines signature
218
231
  all_output_lines << output_lines
219
232
  all_output_lines.flatten!
220
233
  all_trusted &&= trusted
@@ -229,7 +242,7 @@ EOS
229
242
  end
230
243
 
231
244
  if valid || !unknown
232
- summary_line = simplify_sig_line(verify_result.signatures[0].to_s, all_trusted)
245
+ summary_line = simplify_sig_line(verify_result.signatures[0].to_s.dup, all_trusted)
233
246
  end
234
247
 
235
248
  if all_output_lines.length == 0
@@ -242,6 +255,8 @@ EOS
242
255
  end
243
256
  elsif !unknown
244
257
  Chunk::CryptoNotice.new(:invalid, summary_line, all_output_lines)
258
+ elsif unknown_fingerprint
259
+ Chunk::CryptoNotice.new(:unknown_key, "Unable to determine validity of cryptographic signature", all_output_lines, unknown_fingerprint)
245
260
  else
246
261
  unknown_status all_output_lines
247
262
  end
@@ -351,6 +366,31 @@ EOS
351
366
  [notice, sig, msg]
352
367
  end
353
368
 
369
+ def retrieve fingerprint
370
+ require 'net/http'
371
+ uri = URI($config[:keyserver_url] || KEYSERVER_URL)
372
+ unless uri.scheme == "http" and not uri.host.nil?
373
+ return "Invalid url: #{uri}"
374
+ end
375
+
376
+ fingerprint = "0x" + fingerprint unless fingerprint[0..1] == "0x"
377
+ params = {op: "get", search: fingerprint}
378
+ uri.query = URI.encode_www_form(params)
379
+
380
+ begin
381
+ res = Net::HTTP.get_response(uri)
382
+ rescue SocketError # Host doesn't exist or we couldn't connect
383
+ end
384
+ return "Couldn't get key from keyserver at this address: #{uri}" unless res.is_a?(Net::HTTPSuccess)
385
+
386
+ match = KEY_PATTERN.match(res.body)
387
+ return "No key found" unless match && match.length > 0
388
+
389
+ GPGME::Key.import(match[0])
390
+
391
+ return nil
392
+ end
393
+
354
394
  private
355
395
 
356
396
  def unknown_status lines=[]
@@ -394,6 +434,7 @@ private
394
434
  rescue EOFError
395
435
  from_key = nil
396
436
  first_sig = "No public key available for #{signature.fingerprint}"
437
+ unknown_fpr = signature.fingerprint
397
438
  end
398
439
 
399
440
  time_line = "Signature made " + signature.timestamp.strftime("%a %d %b %Y %H:%M:%S %Z") +
@@ -422,7 +463,7 @@ private
422
463
  output_lines << HookManager.run("sig-output",
423
464
  {:signature => signature, :from_key => from_key})
424
465
  end
425
- return output_lines, trusted
466
+ return output_lines, trusted, unknown_fpr
426
467
  end
427
468
 
428
469
  def key_type key, fpr
@@ -435,6 +476,7 @@ private
435
476
  when GPGME::PK_DSA then "DSA "
436
477
  when GPGME::PK_ELG then "ElGamel "
437
478
  when GPGME::PK_ELG_E then "ElGamel "
479
+ else "unknown key type (#{subkey.pubkey_algo}) "
438
480
  end
439
481
  end
440
482
 
@@ -443,7 +485,7 @@ private
443
485
  # elsif only one account, then leave blank so gpg default will be user
444
486
  # else set --local-user from_email_address
445
487
  # NOTE: multiple signers doesn't seem to work with gpgme (2.0.2, 1.0.8)
446
- #
488
+ #
447
489
  def gen_sign_user_opts from
448
490
  account = AccountManager.account_for from
449
491
  account ||= AccountManager.default_account
@@ -16,7 +16,7 @@ class DraftManager
16
16
  def write_draft
17
17
  offset = @source.gen_offset
18
18
  fn = @source.fn_for_offset offset
19
- File.open(fn, "w") { |f| yield f }
19
+ File.open(fn, "w:UTF-8") { |f| yield f }
20
20
  PollManager.poll_from @source
21
21
  end
22
22
 
@@ -33,7 +33,7 @@ class DraftLoader < Source
33
33
  yaml_properties
34
34
 
35
35
  def initialize dir=Redwood::DRAFT_DIR
36
- Dir.mkdir dir unless File.exists? dir
36
+ Dir.mkdir dir unless File.exist? dir
37
37
  super DraftManager.source_name, true, false
38
38
  @dir = dir
39
39
  @cur_offset = 0
@@ -61,8 +61,8 @@ class DraftLoader < Source
61
61
  end
62
62
 
63
63
  def gen_offset
64
- i = 0
65
- while File.exists? fn_for_offset(i)
64
+ i = @cur_offset
65
+ while File.exist? fn_for_offset(i)
66
66
  i += 1
67
67
  end
68
68
  i
@@ -75,7 +75,7 @@ class DraftLoader < Source
75
75
  end
76
76
 
77
77
  def load_message offset
78
- raise SourceError, "Draft not found" unless File.exists? fn_for_offset(offset)
78
+ raise SourceError, "Draft not found" unless File.exist? fn_for_offset(offset)
79
79
  File.open fn_for_offset(offset) do |f|
80
80
  RMail::Mailbox::MBoxReader.new(f).each_message do |input|
81
81
  return RMail::Parser.read(input)
@@ -85,7 +85,7 @@ class DraftLoader < Source
85
85
 
86
86
  def raw_header offset
87
87
  ret = ""
88
- File.open fn_for_offset(offset) do |f|
88
+ File.open(fn_for_offset(offset), "r:UTF-8") do |f|
89
89
  until f.eof? || (l = f.gets) =~ /^$/
90
90
  ret += l
91
91
  end
@@ -94,13 +94,13 @@ class DraftLoader < Source
94
94
  end
95
95
 
96
96
  def each_raw_message_line offset
97
- File.open(fn_for_offset(offset)) do |f|
97
+ File.open(fn_for_offset(offset), "r:UTF-8") do |f|
98
98
  yield f.gets until f.eof?
99
99
  end
100
100
  end
101
101
 
102
102
  def raw_message offset
103
- IO.read(fn_for_offset(offset))
103
+ IO.read(fn_for_offset(offset), :encoding => "UTF-8")
104
104
  end
105
105
 
106
106
  def start_offset; 0; end
@@ -83,7 +83,7 @@ class HookManager
83
83
  @contexts = {}
84
84
  @tags = {}
85
85
 
86
- Dir.mkdir dir unless File.exists? dir
86
+ Dir.mkdir dir unless File.exist? dir
87
87
  end
88
88
 
89
89
  attr_reader :tags
@@ -109,20 +109,20 @@ class HookManager
109
109
  @descs[name] = desc
110
110
  end
111
111
 
112
- def print_hooks f=$stdout
113
- puts <<EOS
114
- Have #{HookManager.descs.size} registered hooks:
115
-
116
- EOS
117
-
118
- HookManager.descs.sort.each do |name, desc|
119
- f.puts <<EOS
112
+ def print_hooks pattern="", f=$stdout
113
+ matching_hooks = HookManager.descs.sort.keep_if {|name, desc| pattern.empty? or name.match(pattern)}.map do |name, desc|
114
+ <<EOS
120
115
  #{name}
121
116
  #{"-" * name.length}
122
117
  File: #{fn_for name}
123
118
  #{desc}
124
119
  EOS
125
120
  end
121
+
122
+ showing_str = matching_hooks.size == HookManager.descs.size ? "" : " (showing #{matching_hooks.size})"
123
+ f.puts "Have #{HookManager.descs.size} registered hooks#{showing_str}:"
124
+ f.puts
125
+ matching_hooks.each { |text| f.puts text }
126
126
  end
127
127
 
128
128
  def enabled? name; !hook_for(name).nil? end