git-commit-notifier 0.11.2 → 0.11.3
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/git-commit-notifier.gemspec +1 -1
- data/lib/git_commit_notifier/commit_hook.rb +46 -15
- data/lib/git_commit_notifier/diff_callback.rb +7 -1
- data/lib/git_commit_notifier/diff_to_html.rb +83 -66
- data/lib/git_commit_notifier/emailer.rb +83 -16
- data/lib/git_commit_notifier/escape_helper.rb +11 -2
- data/lib/git_commit_notifier/executor.rb +1 -1
- data/lib/git_commit_notifier/git.rb +43 -32
- data/lib/git_commit_notifier/result_processor.rb +7 -5
- data/spec/lib/git_commit_notifier/commit_hook_spec.rb +1 -1
- data/spec/lib/git_commit_notifier/diff_to_html_spec.rb +5 -5
- data/spec/lib/git_commit_notifier/emailer_spec.rb +4 -0
- data/spec/lib/git_commit_notifier/git_spec.rb +3 -3
- metadata +29 -32
@@ -2,32 +2,51 @@
|
|
2
2
|
|
3
3
|
require 'premailer'
|
4
4
|
|
5
|
+
# Represents email sender.
|
5
6
|
class GitCommitNotifier::Emailer
|
6
|
-
|
7
|
-
|
8
|
-
|
7
|
+
# Default CSS stylesheet file path
|
8
|
+
DEFAULT_STYLESHEET_PATH = File.join(File.dirname(__FILE__), *'../../template/styles.css'.split('/')).freeze
|
9
|
+
# Default ERB template file path
|
10
|
+
TEMPLATE = File.join(File.dirname(__FILE__), *'../../template/email.html.erb'.split('/')).freeze
|
11
|
+
# Instance variable names
|
12
|
+
PARAMETERS = %w[project_path recipient from_address from_alias date subject text_message html_message repo_name ref_name old_rev new_rev].freeze
|
9
13
|
|
14
|
+
# Gets config.
|
15
|
+
# @return [Hash] Configuration
|
16
|
+
# @note Helper that represents class method in instance scope.
|
17
|
+
# @see GitCommitNotifier::Emailer.config
|
10
18
|
def config
|
11
|
-
|
19
|
+
GitCommitNotifier::Emailer.config
|
12
20
|
end
|
13
21
|
|
14
22
|
def initialize(config, options = {})
|
15
|
-
|
23
|
+
GitCommitNotifier::Emailer.config = config || {}
|
16
24
|
PARAMETERS.each do |name|
|
17
25
|
instance_variable_set("@#{name}".to_sym, options[name.to_sym])
|
18
26
|
end
|
19
27
|
end
|
20
28
|
|
21
29
|
class << self
|
30
|
+
# [Hash] Gets or sets config.
|
31
|
+
attr_accessor :config
|
32
|
+
|
33
|
+
# Resets compiled template.
|
34
|
+
# @note Useful for tests.
|
35
|
+
# @return [NilClass] nil
|
22
36
|
def reset_template
|
23
37
|
@template = nil
|
24
38
|
end
|
25
39
|
|
40
|
+
# Reads template source code from file system.
|
41
|
+
# @return [String] Template source code.
|
26
42
|
def template_source
|
27
|
-
template_file =
|
43
|
+
template_file = config['custom_template'] || TEMPLATE
|
28
44
|
IO.read(template_file)
|
29
45
|
end
|
30
46
|
|
47
|
+
# Gets or reads compiled template.
|
48
|
+
# @return [Object] Compiled template.
|
49
|
+
# @note Erubis used as template engine if present; ERB otherwise.
|
31
50
|
def template
|
32
51
|
unless @template
|
33
52
|
source = template_source
|
@@ -41,8 +60,30 @@ class GitCommitNotifier::Emailer
|
|
41
60
|
end
|
42
61
|
@template
|
43
62
|
end
|
63
|
+
|
64
|
+
# Resets CSS stylesheet source.
|
65
|
+
# @note Useful for tests.
|
66
|
+
# @return [NilClass] nil
|
67
|
+
def reset_stylesheet
|
68
|
+
@stylesheet = nil
|
69
|
+
end
|
70
|
+
|
71
|
+
# Reads CSS stylesheet source code.
|
72
|
+
# @return [String] Stylesheet source code.
|
73
|
+
def stylesheet_source
|
74
|
+
stylesheet = config['stylesheet'] || DEFAULT_STYLESHEET_PATH
|
75
|
+
IO.read(stylesheet)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Gets or reads CSS stylesheet.
|
79
|
+
# @return [String] Stylesheet source code.
|
80
|
+
def stylesheet
|
81
|
+
@stylesheet ||= stylesheet_source
|
82
|
+
end
|
44
83
|
end
|
45
84
|
|
85
|
+
# Gets HTML-formatted message.
|
86
|
+
# @return [String] HTML-formatted message.
|
46
87
|
def mail_html_message
|
47
88
|
html = GitCommitNotifier::Emailer.template.result(binding)
|
48
89
|
if config['expand_css'].nil? || config['expand_css']
|
@@ -52,6 +93,16 @@ class GitCommitNotifier::Emailer
|
|
52
93
|
html
|
53
94
|
end
|
54
95
|
|
96
|
+
# Gets stylesheet string.
|
97
|
+
# @note This is helper to provide data from class context.
|
98
|
+
# @see GitCommitNotifier::Emailer.stylesheet
|
99
|
+
# @return [String] Stylesheet source code.
|
100
|
+
def stylesheet_string
|
101
|
+
GitCommitNotifier::Emailer.stylesheet
|
102
|
+
end
|
103
|
+
|
104
|
+
# Gets or creates email part boundary
|
105
|
+
# @return [String] Email part boundary.
|
55
106
|
def boundary
|
56
107
|
return @boundary if @boundary
|
57
108
|
srand
|
@@ -59,17 +110,17 @@ class GitCommitNotifier::Emailer
|
|
59
110
|
@boundary = Digest::SHA1.hexdigest(seed)
|
60
111
|
end
|
61
112
|
|
62
|
-
|
63
|
-
|
64
|
-
IO.read(stylesheet)
|
65
|
-
end
|
66
|
-
|
113
|
+
# Performs email delivery in debug mode (to STDOUT).
|
114
|
+
# @return [NilClass] nil
|
67
115
|
def perform_delivery_debug(content)
|
68
116
|
content.each do |line|
|
69
117
|
puts line
|
70
118
|
end
|
119
|
+
nil
|
71
120
|
end
|
72
121
|
|
122
|
+
# Performs email delivery through SMTP.
|
123
|
+
# @return [NilClass] nil
|
73
124
|
def perform_delivery_smtp(content, smtp_settings)
|
74
125
|
settings = { }
|
75
126
|
%w(address port domain user_name password authentication enable_tls).each do |key|
|
@@ -94,8 +145,11 @@ class GitCommitNotifier::Emailer
|
|
94
145
|
end
|
95
146
|
end
|
96
147
|
end
|
148
|
+
nil
|
97
149
|
end
|
98
150
|
|
151
|
+
# Performs email delivery through Sendmail.
|
152
|
+
# @return [NilClass] nil
|
99
153
|
def perform_delivery_sendmail(content, options = nil)
|
100
154
|
sendmail_settings = {
|
101
155
|
'location' => "/usr/sbin/sendmail",
|
@@ -108,28 +162,38 @@ class GitCommitNotifier::Emailer
|
|
108
162
|
end
|
109
163
|
f.flush
|
110
164
|
end
|
165
|
+
nil
|
111
166
|
end
|
112
167
|
|
168
|
+
# Performs email delivery through NNTP.
|
169
|
+
# @return [NilClass] nil
|
113
170
|
def perform_delivery_nntp(content, nntp_settings)
|
114
171
|
require 'nntp'
|
115
172
|
Net::NNTP.start(nntp_settings['address'], nntp_settings['port']) do |nntp|
|
116
173
|
nntp.post content
|
117
174
|
end
|
175
|
+
nil
|
118
176
|
end
|
119
177
|
|
178
|
+
# Creates email message and sends it using configured delivery method.
|
179
|
+
# @return [NilClass] nil
|
120
180
|
def send
|
121
181
|
to_tag = config['delivery_method'] == 'nntp' ? 'Newsgroups' : 'To'
|
122
182
|
quoted_from_alias = !@from_alias.nil? ? quote_if_necessary("#{@from_alias}",'utf-8') : nil
|
123
183
|
from = (@from_alias.nil? || @from_alias.empty?) ? @from_address : "#{quoted_from_alias} <#{@from_address}>"
|
124
|
-
|
184
|
+
|
125
185
|
plaintext = if config['add_plaintext'].nil? || config['add_plaintext']
|
126
186
|
@text_message
|
127
187
|
else
|
128
188
|
"Plain text part omitted. Consider setting add_plaintext in configuration."
|
129
189
|
end
|
130
|
-
|
190
|
+
|
131
191
|
content = []
|
132
|
-
content << "From: #{from}"
|
192
|
+
content << "From: #{from}" unless from.nil?
|
193
|
+
|
194
|
+
# Setting the email date from the commit date is undesired by those
|
195
|
+
# who sort their email by send date instead of receive date
|
196
|
+
#content << "Date: #{@date}" if !@date.nil?
|
133
197
|
|
134
198
|
content.concat [
|
135
199
|
"#{to_tag}: #{quote_if_necessary(@recipient, 'utf-8')}",
|
@@ -168,10 +232,11 @@ class GitCommitNotifier::Emailer
|
|
168
232
|
else # sendmail
|
169
233
|
perform_delivery_sendmail(content, config['sendmail_options'])
|
170
234
|
end
|
235
|
+
nil
|
171
236
|
end
|
172
237
|
|
173
238
|
# Convert a message into quoted printable encoding,
|
174
|
-
# limiting line length to 76 characters per spec
|
239
|
+
# limiting line length to 76 characters per spec.
|
175
240
|
# Encoding messages in this way ensures that they
|
176
241
|
# won't violate rules for maximum line length, which
|
177
242
|
# can result in the MTA breaking lines at inconvenient points,
|
@@ -180,7 +245,7 @@ class GitCommitNotifier::Emailer
|
|
180
245
|
StringIO.open("", "w") do |output|
|
181
246
|
# Character encoding of output string can be plain US-ASCII since quoted-printable is plain ASCII
|
182
247
|
output.string.force_encoding("US-ASCII") if output.string.respond_to?(:force_encoding)
|
183
|
-
|
248
|
+
|
184
249
|
line_max = 76
|
185
250
|
line_len = 0
|
186
251
|
|
@@ -239,3 +304,5 @@ class GitCommitNotifier::Emailer
|
|
239
304
|
end
|
240
305
|
end
|
241
306
|
|
307
|
+
|
308
|
+
|
@@ -1,7 +1,12 @@
|
|
1
|
-
# -*- coding: utf-8; mode: ruby; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2
|
1
|
+
# -*- coding: utf-8; mode: ruby; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2
|
2
|
+
# -*- vim:fenc=utf-8:filetype=ruby:et:sw=2:ts=2:sts=2
|
2
3
|
|
4
|
+
# Provides content escaping helpers.
|
3
5
|
module GitCommitNotifier::EscapeHelper
|
4
|
-
|
6
|
+
# Expand tabs into spaces.
|
7
|
+
# @param [String] s Text to be expanded.
|
8
|
+
# @param [FixNum] tabWidth One tab indentation (in count of spaces).
|
9
|
+
# @return [String] Expanded text.
|
5
10
|
def expand_tabs(s, tabWidth)
|
6
11
|
delta = 0
|
7
12
|
s.gsub(/\t/) do |m|
|
@@ -11,6 +16,10 @@ module GitCommitNotifier::EscapeHelper
|
|
11
16
|
end
|
12
17
|
end
|
13
18
|
|
19
|
+
# Escapes expanded content using CGI.escapeHTML and {#expand_tabs}.
|
20
|
+
# @param [String] s Text to be expanded and extended.
|
21
|
+
# @return [String] Escaped and expanded text.
|
22
|
+
# @see #expand_tabs
|
14
23
|
def escape_content(s)
|
15
24
|
CGI.escapeHTML(expand_tabs(s, 4)).gsub(" ", " ")
|
16
25
|
end
|
@@ -21,7 +21,7 @@ module GitCommitNotifier
|
|
21
21
|
GitCommitNotifier::CommitHook.show_error("No data given on standard input")
|
22
22
|
return
|
23
23
|
end
|
24
|
-
|
24
|
+
|
25
25
|
# Note that there may be multiple lines on stdin, such
|
26
26
|
# as in the case of multiple tags being pushed
|
27
27
|
$stdin.each_line do |line|
|
@@ -5,17 +5,28 @@ require 'set'
|
|
5
5
|
# Git methods
|
6
6
|
class GitCommitNotifier::Git
|
7
7
|
class << self
|
8
|
-
# Runs specified command.
|
8
|
+
# Runs specified command and gets its output.
|
9
9
|
# @return (String) Shell command STDOUT (forced to UTF-8)
|
10
10
|
# @raise [ArgumentError] when command exits with nonzero status.
|
11
11
|
def from_shell(cmd)
|
12
12
|
r = `#{cmd}`
|
13
|
-
raise ArgumentError.new("#{cmd} failed")
|
13
|
+
raise ArgumentError.new("#{cmd} failed") unless $?.exitstatus.zero?
|
14
14
|
r.force_encoding(Encoding::UTF_8) if r.respond_to?(:force_encoding)
|
15
15
|
r
|
16
16
|
end
|
17
17
|
|
18
|
-
#
|
18
|
+
# Runs specified command and gets its output as array of lines.
|
19
|
+
# @return (Enumerable(String)) Shell command STDOUT (forced to UTF-8) as enumerable lines.
|
20
|
+
# @raise [ArgumentError] when command exits with nonzero status.
|
21
|
+
# @see from_shell
|
22
|
+
def lines_from_shell(cmd)
|
23
|
+
lines = from_shell(cmd)
|
24
|
+
# Ruby 1.9 tweak.
|
25
|
+
lines = lines.lines if lines.respond_to?(:lines)
|
26
|
+
lines
|
27
|
+
end
|
28
|
+
|
29
|
+
# Runs `git show`
|
19
30
|
# @note uses "--pretty=fuller" option.
|
20
31
|
# @return [String] Its output
|
21
32
|
# @see from_shell
|
@@ -23,34 +34,30 @@ class GitCommitNotifier::Git
|
|
23
34
|
# @param [Hash] opts Options
|
24
35
|
# @option opts [Boolean] :ignore_whitespaces Ignore whitespaces or not
|
25
36
|
def show(rev, opts = {})
|
26
|
-
gitopt = ""
|
37
|
+
gitopt = " --date=rfc2822"
|
27
38
|
gitopt += " --pretty=fuller"
|
28
39
|
gitopt += " -w" if opts[:ignore_whitespaces]
|
29
|
-
|
30
|
-
data
|
40
|
+
from_shell("git show #{rev.strip}#{gitopt}")
|
31
41
|
end
|
32
42
|
|
33
|
-
#
|
43
|
+
# Runs `git log`
|
34
44
|
# @note uses "--pretty=fuller" option.
|
35
45
|
# @return [String] Its output
|
36
46
|
# @see from_shell
|
37
47
|
# @param [String] rev1 First revision
|
38
48
|
# @param [String] rev2 Second revision
|
39
49
|
def log(rev1, rev2)
|
40
|
-
|
41
|
-
data
|
50
|
+
from_shell("git log --pretty=fuller #{rev1}..#{rev2}").strip
|
42
51
|
end
|
43
52
|
|
44
|
-
#
|
53
|
+
# Runs `git log` and extract filenames only
|
45
54
|
# @note uses "--pretty=fuller" and "--name-status" options.
|
46
55
|
# @return [Array(String)] File names
|
47
|
-
# @see
|
56
|
+
# @see lines_from_shell
|
48
57
|
# @param [String] rev1 First revision
|
49
58
|
# @param [String] rev2 Second revision
|
50
59
|
def changed_files(rev1, rev2)
|
51
|
-
|
52
|
-
lines = from_shell("git log #{rev1}..#{rev2} --name-status --pretty=oneline")
|
53
|
-
lines = lines.lines if lines.respond_to?(:lines)
|
60
|
+
lines = lines_from_shell("git log #{rev1}..#{rev2} --name-status --pretty=oneline")
|
54
61
|
lines = lines.select {|line| line =~ /^\w{1}\s+\w+/} # grep out only filenames
|
55
62
|
lines.uniq
|
56
63
|
end
|
@@ -59,18 +66,16 @@ class GitCommitNotifier::Git
|
|
59
66
|
args = branch_heads - [ branch_head(treeish) ]
|
60
67
|
args.map! { |tree| "^#{tree}" }
|
61
68
|
args << treeish
|
62
|
-
lines =
|
63
|
-
lines = lines.lines if lines.respond_to?(:lines)
|
69
|
+
lines = lines_from_shell("git rev-list #{args.join(' ')}")
|
64
70
|
lines.to_a.map { |commit| commit.chomp }
|
65
71
|
end
|
66
72
|
|
67
73
|
def branch_heads
|
68
|
-
lines =
|
69
|
-
lines = lines.lines if lines.respond_to?(:lines)
|
74
|
+
lines = lines_from_shell("git rev-parse --branches")
|
70
75
|
lines.to_a.map { |head| head.chomp }
|
71
76
|
end
|
72
77
|
|
73
|
-
def git_dir
|
78
|
+
def git_dir
|
74
79
|
from_shell("git rev-parse --git-dir").strip
|
75
80
|
end
|
76
81
|
|
@@ -81,37 +86,36 @@ class GitCommitNotifier::Git
|
|
81
86
|
def branch_head(treeish)
|
82
87
|
from_shell("git rev-parse #{treeish}").strip
|
83
88
|
end
|
84
|
-
|
89
|
+
|
85
90
|
def new_commits(oldrev, newrev, refname, unique_to_current_branch)
|
86
91
|
# We want to get the set of commits (^B1 ^B2 ... ^oldrev newrev)
|
87
92
|
# Where B1, B2, ..., are any other branch
|
88
|
-
|
89
93
|
s = Set.new
|
90
|
-
|
94
|
+
|
91
95
|
# If we want to include only those commits that are
|
92
96
|
# unique to this branch, then exclude commits that occur on
|
93
97
|
# other branches
|
94
98
|
if unique_to_current_branch
|
95
99
|
# Make a set of all branches, not'd (^BCURRENT ^B1 ^B2...)
|
96
|
-
not_branches =
|
97
|
-
s.merge(not_branches.
|
98
|
-
|
100
|
+
not_branches = lines_from_shell("git rev-parse --not --branches")
|
101
|
+
s.merge(not_branches.to_a.map { |l| l.chomp }.to_set)
|
102
|
+
|
99
103
|
# Remove the current branch (^BCURRENT) from the set
|
100
104
|
current_branch = rev_parse(refname)
|
101
105
|
s.delete("^#{current_branch}")
|
102
106
|
end
|
103
|
-
|
107
|
+
|
104
108
|
# Add not'd oldrev (^oldrev)
|
105
|
-
s.add("^#{oldrev}")
|
109
|
+
s.add("^#{oldrev}") unless oldrev =~ /^0+$/
|
106
110
|
|
107
111
|
# Add newrev
|
108
112
|
s.add(newrev)
|
109
|
-
|
113
|
+
|
110
114
|
# We should now have ^B1... ^oldrev newrev
|
111
115
|
|
112
116
|
# Get all the commits that match that specification
|
113
|
-
lines =
|
114
|
-
commits = lines.
|
117
|
+
lines = lines_from_shell("git rev-list --reverse #{s.to_a.join(' ')}")
|
118
|
+
commits = lines.to_a.map { |l| l.chomp }
|
115
119
|
end
|
116
120
|
|
117
121
|
def rev_type(rev)
|
@@ -119,7 +123,7 @@ class GitCommitNotifier::Git
|
|
119
123
|
rescue ArgumentError
|
120
124
|
nil
|
121
125
|
end
|
122
|
-
|
126
|
+
|
123
127
|
def tag_info(refname)
|
124
128
|
fields = [
|
125
129
|
':tagobject => %(*objectname)',
|
@@ -134,16 +138,23 @@ class GitCommitNotifier::Git
|
|
134
138
|
eval(hash_script)
|
135
139
|
end
|
136
140
|
|
141
|
+
# Gets repository name.
|
142
|
+
# @note Tries to gets human readable repository name through `git config hooks.emailprefix` call.
|
143
|
+
# If it's not specified then returns directory name (except '.git' suffix if exists).
|
144
|
+
# @return [String] Human readable repository name.
|
137
145
|
def repo_name
|
138
146
|
git_prefix = begin
|
139
147
|
from_shell("git config hooks.emailprefix").strip
|
140
148
|
rescue ArgumentError
|
141
149
|
''
|
142
150
|
end
|
143
|
-
return git_prefix
|
151
|
+
return git_prefix unless git_prefix.empty?
|
144
152
|
File.expand_path(git_dir).split("/").last.sub(/\.git$/, '')
|
145
153
|
end
|
146
154
|
|
155
|
+
# Gets mailing list address.
|
156
|
+
# @note mailing list address retrieved through `git config hooks.mailinglist` call.
|
157
|
+
# @return [String] Mailing list address if exists; otherwise nil.
|
147
158
|
def mailing_list_address
|
148
159
|
from_shell("git config hooks.mailinglist").strip
|
149
160
|
rescue ArgumentError
|
@@ -4,19 +4,21 @@ require 'cgi'
|
|
4
4
|
require 'git_commit_notifier/escape_helper'
|
5
5
|
|
6
6
|
module GitCommitNotifier
|
7
|
+
# Processes diff result
|
8
|
+
# input (loaded in @diff) is an array having Hash elements:
|
9
|
+
# { :action => action, :token => string }
|
10
|
+
# action can be :discard_a, :discard_b or :match
|
11
|
+
# output: two formatted html strings, one for the removals and one for the additions
|
7
12
|
class ResultProcessor
|
8
13
|
include EscapeHelper
|
9
|
-
# input (loaded in @diff) is an array having Hash elements:
|
10
|
-
# { :action => action, :token => string }
|
11
|
-
# action can be :discard_a, :discard_b or :match
|
12
|
-
|
13
|
-
# output: two formatted html strings, one for the removals and one for the additions
|
14
14
|
|
15
15
|
def results
|
16
16
|
close_tags # close last tag
|
17
17
|
[array_of_lines(@result[:removal]), array_of_lines(@result[:addition])]
|
18
18
|
end
|
19
19
|
|
20
|
+
# Gets length of tokenized diff in characters.
|
21
|
+
# @return [FixNum] Length of the tokenized diff in characters.
|
20
22
|
def length_in_chars(diff)
|
21
23
|
diff.inject(0) do |length, s|
|
22
24
|
token = s[:token]
|
@@ -74,7 +74,7 @@ describe GitCommitNotifier::CommitHook do
|
|
74
74
|
def expect_repository_access
|
75
75
|
mock(GitCommitNotifier::Git).rev_type(REVISIONS.first) { "commit" }
|
76
76
|
mock(GitCommitNotifier::Git).rev_type(REVISIONS.last) { "commit" }
|
77
|
-
mock(GitCommitNotifier::Git).new_commits(anything, anything, anything, anything) { REVISIONS }
|
77
|
+
mock(GitCommitNotifier::Git).new_commits(anything, anything, anything, anything) { REVISIONS }
|
78
78
|
mock(GitCommitNotifier::Git).mailing_list_address { 'recipient@test.com' }
|
79
79
|
mock(GitCommitNotifier::Git).repo_name { 'testproject' }
|
80
80
|
mock(GitCommitNotifier::Git).changed_files('7e4f6b4', '4f13525') { [] }
|