git-commit-mailer 1.0.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1a9a76ede1a8e869ceb08833b8d53d8ac98a4a18
4
+ data.tar.gz: cafcfc3b82ab6101a2b57b7d9eb176afac78f047
5
+ SHA512:
6
+ metadata.gz: 3b582c308654f80df51c2f3804b1d4ee97a068432e79ab13ddd4e23f1339efb961a2bbe39cbf60399adeb34570ef531f14592938368d2136e6c3de63a50df291
7
+ data.tar.gz: 87f2bc5f3cc64241d0a443a66a1e1d9f8027751928cac405f11a47943923f1bb413ecf713f968d32e5da5fe588d79a9284e2040d30998d3a9b9b37050e330255
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ /.bundle/config
2
+ /Gemfile.lock
3
+ /vendor/
4
+ /test/tmp/
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ rvm:
2
+ - 2.0.0
3
+ - 2.1
4
+ - 2.2
5
+ notifications:
6
+ email:
7
+ recipients:
8
+ - commit@clear-code.com
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ [![Build Status](https://travis-ci.org/clear-code/git-commit-mailer.svg?branch=master)](https://travis-ci.org/clear-code/git-commit-mailer)
2
+
3
+ # GitCommitMailer
4
+
5
+ A utility to send commit mails for commits pushed to git repositories.
6
+
7
+ See also [Git](http://git-scm.com/).
8
+
9
+ ## Authors
10
+
11
+ * Kouhei Sutou <kou@clear-code.com>
12
+ * Ryo Onodera <onodera@clear-code.com>
13
+ * Kenji Okimoto <okimoto@clear-code.com>
14
+
15
+ ## License
16
+
17
+ GitCommitMailer is licensed under GPLv3 or later. See
18
+ license/GPL-3.txt for details.
19
+
20
+ ## Dependencies
21
+
22
+ * Ruby >= 2.0.0
23
+ * git >= 1.7
24
+
25
+ ## Install
26
+
27
+ ~~~
28
+ $ gem install git-commit-mailer
29
+ ~~~
30
+
31
+ git-commit-mailer utilizes git's hook functionality to send
32
+ commit mails.
33
+
34
+ Edit "post-receive" shell script file to execute it from there,
35
+ which is located under "hooks" directory in a git repository.
36
+
37
+ Example:
38
+
39
+ ~~~
40
+ git-commit-mailer \
41
+ --from-domain=example.com \
42
+ --error-to=onodera@example.com \
43
+ commit@example.com
44
+ ~~~
45
+
46
+ For more detailed usage and options, execute commit-email.rb
47
+ with `--help` option.
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
2
+ #
3
+ # This program is free software: you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as published by
5
+ # the Free Software Foundation, either version 3 of the License, or
6
+ # (at your option) any later version.
7
+ #
8
+ # This program is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
15
+
16
+ require "bundler/gem_tasks"
17
+
18
+ task :default => :test
19
+
20
+ desc "Run test"
21
+ task :test do
22
+ ruby("test/run-test.rb")
23
+ end
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (C) 2009 Ryo Onodera <onodera@clear-code.com>
5
+ # Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
6
+ #
7
+ # This program is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This program is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+
20
+ # See also post-receive-email in git for git repository
21
+ # change detection:
22
+ # http://git.kernel.org/?p=git/git.git;a=blob;f=contrib/hooks/post-receive-email
23
+
24
+ require "git-commit-mailer"
25
+
26
+
27
+ begin
28
+ argv = []
29
+ processing_change = nil
30
+
31
+ found_include_option = false
32
+ ARGV.each do |arg|
33
+ if found_include_option
34
+ $LOAD_PATH.unshift(arg)
35
+ found_include_option = false
36
+ else
37
+ case arg
38
+ when "-I", "--include"
39
+ found_include_option = true
40
+ when /\A-I/, /\A--include=?/
41
+ path = $POSTMATCH
42
+ $LOAD_PATH.unshift(path) unless path.empty?
43
+ else
44
+ argv << arg
45
+ end
46
+ end
47
+ end
48
+
49
+ mailer = GitCommitMailer.parse_options_and_create(argv)
50
+
51
+ if not mailer.track_remote?
52
+ running = SpentTime.new("running the whole command")
53
+ running.spend do
54
+ while line = STDIN.gets
55
+ old_revision, new_revision, reference = line.split
56
+ processing_change = [old_revision, new_revision, reference]
57
+ mailer.process_reference_change(old_revision, new_revision, reference)
58
+ mailer.send_all_mails
59
+ end
60
+ end
61
+
62
+ if mailer.verbose?
63
+ $executing_git.report
64
+ $sending_mail.report
65
+ running.report
66
+ end
67
+ else
68
+ reference_changes = mailer.fetch
69
+ reference_changes.each do |old_revision, new_revision, reference|
70
+ processing_change = [old_revision, new_revision, reference]
71
+ mailer.process_reference_change(old_revision, new_revision, reference)
72
+ mailer.send_all_mails
73
+ end
74
+ end
75
+ rescue Exception => error
76
+ require 'net/smtp'
77
+ require 'socket'
78
+ require 'etc'
79
+
80
+ to = []
81
+ subject = "Error"
82
+ user = Etc.getpwuid(Process.uid).name
83
+ from = "#{user}@#{Socket.gethostname}"
84
+ sender = nil
85
+ server = nil
86
+ port = nil
87
+ begin
88
+ to, options = GitCommitMailer.parse(argv)
89
+ to = options.error_to unless options.error_to.empty?
90
+ from = options.from || from
91
+ sender = options.sender
92
+ subject = "#{options.name}: #{subject}" if options.name
93
+ server = options.server
94
+ port = options.port
95
+ rescue OptionParser::MissingArgument
96
+ argv.delete_if {|argument| $!.args.include?(argument)}
97
+ retry
98
+ rescue OptionParser::ParseError
99
+ if to.empty?
100
+ _to, *_ = ARGV.reject {|argument| /^-/.match(argument)}
101
+ to = [_to]
102
+ end
103
+ end
104
+
105
+ detail = <<-EOM
106
+ Processing change: #{processing_change.inspect}
107
+
108
+ #{error.class}: #{error.message}
109
+ #{error.backtrace.join("\n")}
110
+ EOM
111
+ to = to.compact
112
+ if to.empty?
113
+ STDERR.puts detail
114
+ else
115
+ from = GitCommitMailer.extract_email_address(from)
116
+ to = to.collect {|address| GitCommitMailer.extract_email_address(address)}
117
+ header = <<-HEADER
118
+ X-Mailer: #{GitCommitMailer.x_mailer}
119
+ MIME-Version: 1.0
120
+ Content-Type: text/plain; charset=us-ascii
121
+ Content-Transfer-Encoding: 7bit
122
+ From: #{from}
123
+ To: #{to.join(', ')}
124
+ Subject: #{subject}
125
+ Date: #{Time.now.rfc2822}
126
+ HEADER
127
+ header << "Sender: #{sender}\n" if sender
128
+ mail = <<-MAIL
129
+ #{header}
130
+
131
+ #{detail}
132
+ MAIL
133
+ GitCommitMailer.send_mail(server || "localhost", port,
134
+ sender || from, to, mail)
135
+ exit(false)
136
+ end
137
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'git-commit-mailer/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "git-commit-mailer"
8
+ spec.version = GitCommitMailer::VERSION
9
+ spec.authors = ["Ryo Onodera", "Kouhei Sutou", "Kenji Okimoto"]
10
+ spec.email = ["onodera@clear-code.com", "kou@clear-code.com", "okimoto@clear-code.com"]
11
+
12
+ spec.summary = %q{A utility to send commit mails for commits pushed to git repositories.}
13
+ spec.description = %q{A utility to send commit mails for commits pushed to git repositories.}
14
+ spec.homepage = "https://github.com/clear-code/git-commit-mailer"
15
+ spec.license = "GPL-3.0+"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "bin"
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "test-unit"
25
+ spec.add_development_dependency "test-unit-rr"
26
+ end
@@ -0,0 +1,1379 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2009 Ryo Onodera <onodera@clear-code.com>
4
+ # Copyright (C) 2012-2014 Kouhei Sutou <kou@clear-code.com>
5
+ #
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ # See also post-receive-email in git for git repository
20
+ # change detection:
21
+ # http://git.kernel.org/?p=git/git.git;a=blob;f=contrib/hooks/post-receive-email
22
+
23
+ require "English"
24
+ require "optparse"
25
+ require "ostruct"
26
+ require "time"
27
+ require "net/smtp"
28
+ require "socket"
29
+ require "nkf"
30
+ require "shellwords"
31
+ require "erb"
32
+ require "digest"
33
+
34
+ require "git-commit-mailer/info"
35
+ require "git-commit-mailer/push-info"
36
+ require "git-commit-mailer/commit-info"
37
+
38
+ class SpentTime
39
+ def initialize(label)
40
+ @label = label
41
+ @seconds = 0.0
42
+ end
43
+
44
+ def spend
45
+ start_time = Time.now
46
+ returned_object = yield
47
+ @seconds += (Time.now - start_time)
48
+ returned_object
49
+ end
50
+
51
+ def report
52
+ puts "#{"%0.9s" % @seconds} seconds spent by #{@label}."
53
+ end
54
+ end
55
+
56
+ class GitCommitMailer
57
+ KILO_SIZE = 1000
58
+ DEFAULT_MAX_SIZE = "100M"
59
+
60
+ class << self
61
+ def x_mailer
62
+ "#{name} #{VERSION}; #{URL}"
63
+ end
64
+
65
+ def execute(command, working_directory=nil, &block)
66
+ if ENV["DEBUG"]
67
+ suppress_stderr = ""
68
+ else
69
+ suppress_stderr = " 2> /dev/null"
70
+ end
71
+
72
+ script = "#{command} #{suppress_stderr}"
73
+ puts script if ENV["DEBUG"]
74
+ result = nil
75
+ with_working_direcotry(working_directory) do
76
+ if block_given?
77
+ IO.popen(script, "w+", &block)
78
+ else
79
+ result = `#{script} 2>&1`
80
+ end
81
+ end
82
+ raise "execute failed: #{command}\n#{result}" unless $?.exitstatus.zero?
83
+ result.force_encoding("UTF-8") if result.respond_to?(:force_encoding)
84
+ result
85
+ end
86
+
87
+ def with_working_direcotry(working_directory)
88
+ if working_directory
89
+ Dir.chdir(working_directory) do
90
+ yield
91
+ end
92
+ else
93
+ yield
94
+ end
95
+ end
96
+
97
+ def shell_escape(string)
98
+ # To suppress warnings from Shellwords::escape.
99
+ if string.respond_to? :force_encoding
100
+ bytes = string.dup.force_encoding("ascii-8bit")
101
+ else
102
+ bytes = string
103
+ end
104
+
105
+ Shellwords.escape(bytes)
106
+ end
107
+
108
+ def git(git_bin_path, repository, command, &block)
109
+ $executing_git ||= SpentTime.new("executing git commands")
110
+ $executing_git.spend do
111
+ execute("#{git_bin_path} --git-dir=#{shell_escape(repository)} #{command}", &block)
112
+ end
113
+ end
114
+
115
+ def short_revision(revision)
116
+ revision[0, 7]
117
+ end
118
+
119
+ def extract_email_address(address)
120
+ if /<(.+?)>/ =~ address
121
+ $1
122
+ else
123
+ address
124
+ end
125
+ end
126
+
127
+ def extract_email_address_from_mail(mail)
128
+ begin
129
+ from_header = mail.lines.grep(/\AFrom: .*\Z/)[0]
130
+ extract_email_address(from_header.rstrip.sub(/From: /, ""))
131
+ rescue
132
+ raise '"From:" header is not found in mail.'
133
+ end
134
+ end
135
+
136
+ def extract_to_addresses(mail)
137
+ to_value = nil
138
+ if /^To:(.*\r?\n(?:^\s+.*)*)/ni =~ mail
139
+ to_value = $1
140
+ else
141
+ raise "'To:' header is not found in mail:\n#{mail}"
142
+ end
143
+ to_value_without_comment = to_value.gsub(/".*?"/n, "")
144
+ to_value_without_comment.split(/\s*,\s*/n).collect do |address|
145
+ extract_email_address(address.strip)
146
+ end
147
+ end
148
+
149
+ def send_mail(server, port, from, to, mail)
150
+ $sending_mail ||= SpentTime.new("sending mails")
151
+ $sending_mail.spend do
152
+ Net::SMTP.start(server, port) do |smtp|
153
+ smtp.open_message_stream(from, to) do |f|
154
+ f.print(mail)
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ def parse_options_and_create(argv=nil)
161
+ argv ||= ARGV
162
+ to, options = parse(argv)
163
+ to += options.to
164
+ mailer = new(to.compact)
165
+ apply_options(mailer, options)
166
+ mailer
167
+ end
168
+
169
+ def parse(argv)
170
+ options = make_options
171
+
172
+ parser = make_parser(options)
173
+ argv = argv.dup
174
+ parser.parse!(argv)
175
+ to = argv
176
+
177
+ [to, options]
178
+ end
179
+
180
+ def format_size(size)
181
+ return "no limit" if size.nil?
182
+ return "#{size}B" if size < KILO_SIZE
183
+ size /= KILO_SIZE.to_f
184
+ return "#{size}KB" if size < KILO_SIZE
185
+ size /= KILO_SIZE.to_f
186
+ return "#{size}MB" if size < KILO_SIZE
187
+ size /= KILO_SIZE.to_f
188
+ "#{size}GB"
189
+ end
190
+
191
+ private
192
+ def apply_options(mailer, options)
193
+ mailer.repository = options.repository
194
+ #mailer.reference = options.reference
195
+ mailer.repository_browser = options.repository_browser
196
+ mailer.github_base_url = options.github_base_url
197
+ mailer.github_user = options.github_user
198
+ mailer.github_repository = options.github_repository
199
+ mailer.gitlab_project_uri = options.gitlab_project_uri
200
+ mailer.send_per_to = options.send_per_to
201
+ mailer.from = options.from
202
+ mailer.from_domain = options.from_domain
203
+ mailer.sender = options.sender
204
+ mailer.add_diff = options.add_diff
205
+ mailer.add_html = options.add_html
206
+ mailer.max_size = options.max_size
207
+ mailer.max_diff_size = options.max_diff_size
208
+ mailer.repository_uri = options.repository_uri
209
+ mailer.rss_path = options.rss_path
210
+ mailer.rss_uri = options.rss_uri
211
+ mailer.show_path = options.show_path
212
+ mailer.send_push_mail = options.send_push_mail
213
+ mailer.name = options.name
214
+ mailer.server = options.server
215
+ mailer.port = options.port
216
+ mailer.date = options.date
217
+ mailer.git_bin_path = options.git_bin_path
218
+ mailer.track_remote = options.track_remote
219
+ mailer.verbose = options.verbose
220
+ mailer.sleep_per_mail = options.sleep_per_mail
221
+ end
222
+
223
+ def parse_size(size)
224
+ case size
225
+ when /\A(.+?)GB?\z/i
226
+ Float($1) * KILO_SIZE ** 3
227
+ when /\A(.+?)MB?\z/i
228
+ Float($1) * KILO_SIZE ** 2
229
+ when /\A(.+?)KB?\z/i
230
+ Float($1) * KILO_SIZE
231
+ when /\A(.+?)B?\z/i
232
+ Float($1)
233
+ else
234
+ raise ArgumentError, "invalid size: #{size.inspect}"
235
+ end
236
+ end
237
+
238
+ def make_options
239
+ options = OpenStruct.new
240
+ options.repository = ".git"
241
+ #options.reference = "refs/heads/master"
242
+ options.repository_browser = nil
243
+ options.github_base_url = "https://github.com"
244
+ options.github_user = nil
245
+ options.github_repository = nil
246
+ options.gitlab_project_uri = nil
247
+ options.to = []
248
+ options.send_per_to = false
249
+ options.error_to = []
250
+ options.from = nil
251
+ options.from_domain = nil
252
+ options.sender = nil
253
+ options.add_diff = true
254
+ options.add_html = false
255
+ options.max_size = parse_size(DEFAULT_MAX_SIZE)
256
+ options.max_diff_size = parse_size(DEFAULT_MAX_SIZE)
257
+ options.repository_uri = nil
258
+ options.rss_path = nil
259
+ options.rss_uri = nil
260
+ options.show_path = false
261
+
262
+ options.send_push_mail = false
263
+ options.name = nil
264
+ options.server = "localhost"
265
+ options.port = Net::SMTP.default_port
266
+ options.date = nil
267
+ options.git_bin_path = "git"
268
+ options.track_remote = false
269
+ options.verbose = false
270
+ options.sleep_per_mail = 0
271
+ options
272
+ end
273
+
274
+ def make_parser(options)
275
+ OptionParser.new do |parser|
276
+ parser.banner += "TO"
277
+
278
+ add_repository_options(parser, options)
279
+ add_email_options(parser, options)
280
+ add_output_options(parser, options)
281
+ add_rss_options(parser, options)
282
+ add_other_options(parser, options)
283
+
284
+ parser.on_tail("--help", "Show this message") do
285
+ puts parser
286
+ exit!
287
+ end
288
+ end
289
+ end
290
+
291
+ def add_repository_options(parser, options)
292
+ parser.separator ""
293
+ parser.separator "Repository related options:"
294
+
295
+ parser.on("--repository=PATH",
296
+ "Use PATH as the target git repository",
297
+ "(#{options.repository})") do |path|
298
+ options.repository = path
299
+ end
300
+
301
+ parser.on("--reference=REFERENCE",
302
+ "Use REFERENCE as the target reference",
303
+ "(#{options.reference})") do |reference|
304
+ options.reference = reference
305
+ end
306
+
307
+ available_software = ["github", "github-wiki", "gitlab"]
308
+ label = available_software.join(", ")
309
+ parser.on("--repository-browser=SOFTWARE",
310
+ available_software,
311
+ "Use SOFTWARE as the repository browser",
312
+ "(available repository browsers: #{label})") do |software|
313
+ options.repository_browser = software
314
+ end
315
+
316
+ add_github_options(parser, options)
317
+ add_gitlab_options(parser, options)
318
+ end
319
+
320
+ def add_github_options(parser, options)
321
+ parser.separator ""
322
+ parser.separator "GitHub related options:"
323
+
324
+ parser.on("--github-base-url=URL",
325
+ "Use URL as base URL of GitHub",
326
+ "(#{options.github_base_url})") do |url|
327
+ options.github_base_url = url
328
+ end
329
+
330
+ parser.on("--github-user=USER",
331
+ "Use USER as the GitHub user") do |user|
332
+ options.github_user = user
333
+ end
334
+
335
+ parser.on("--github-repository=REPOSITORY",
336
+ "Use REPOSITORY as the GitHub repository") do |repository|
337
+ options.github_repository = repository
338
+ end
339
+ end
340
+
341
+ def add_gitlab_options(parser, options)
342
+ parser.separator ""
343
+ parser.separator "GitLab related options:"
344
+
345
+ parser.on("--gitlab-project-uri=URI",
346
+ "Use URI as GitLab project URI") do |uri|
347
+ options.gitlab_project_uri = uri
348
+ end
349
+ end
350
+
351
+ def add_email_options(parser, options)
352
+ parser.separator ""
353
+ parser.separator "E-mail related options:"
354
+
355
+ parser.on("-sSERVER", "--server=SERVER",
356
+ "Use SERVER as SMTP server (#{options.server})") do |server|
357
+ options.server = server
358
+ end
359
+
360
+ parser.on("-pPORT", "--port=PORT", Integer,
361
+ "Use PORT as SMTP port (#{options.port})") do |port|
362
+ options.port = port
363
+ end
364
+
365
+ parser.on("-tTO", "--to=TO", "Add TO to To: address") do |to|
366
+ options.to << to unless to.nil?
367
+ end
368
+
369
+ parser.on("--[no-]send-per-to",
370
+ "Send a mail for each To: address",
371
+ "instead of sending a mail for all To: addresses",
372
+ "(#{options.send_per_to})") do |boolean|
373
+ options.send_per_to = boolean
374
+ end
375
+
376
+ parser.on("-eTO", "--error-to=TO",
377
+ "Add TO to To: address when an error occurs") do |to|
378
+ options.error_to << to unless to.nil?
379
+ end
380
+
381
+ parser.on("-fFROM", "--from=FROM", "Use FROM as from address") do |from|
382
+ if options.from_domain
383
+ raise OptionParser::CannotCoexistOption,
384
+ "cannot coexist with --from-domain"
385
+ end
386
+ options.from = from
387
+ end
388
+
389
+ parser.on("--from-domain=DOMAIN",
390
+ "Use author@DOMAIN as from address") do |domain|
391
+ if options.from
392
+ raise OptionParser::CannotCoexistOption,
393
+ "cannot coexist with --from"
394
+ end
395
+ options.from_domain = domain
396
+ end
397
+
398
+ parser.on("--sender=SENDER",
399
+ "Use SENDER as a sender address") do |sender|
400
+ options.sender = sender
401
+ end
402
+
403
+ parser.on("--sleep-per-mail=SECONDS", Float,
404
+ "Sleep SECONDS seconds after each email sent") do |seconds|
405
+ options.send_per_mail = seconds
406
+ end
407
+ end
408
+
409
+ def add_output_options(parser, options)
410
+ parser.separator ""
411
+ parser.separator "Output related options:"
412
+
413
+ parser.on("--name=NAME", "Use NAME as repository name") do |name|
414
+ options.name = name
415
+ end
416
+
417
+ parser.on("--[no-]show-path",
418
+ "Show commit target path") do |bool|
419
+ options.show_path = bool
420
+ end
421
+
422
+ parser.on("--[no-]send-push-mail",
423
+ "Send push mail") do |bool|
424
+ options.send_push_mail = bool
425
+ end
426
+
427
+ parser.on("--repository-uri=URI",
428
+ "Use URI as URI of repository") do |uri|
429
+ options.repository_uri = uri
430
+ end
431
+
432
+ parser.on("-n", "--no-diff", "Don't add diffs") do |diff|
433
+ options.add_diff = false
434
+ end
435
+
436
+ parser.on("--[no-]add-html",
437
+ "Add HTML as alternative content") do |add_html|
438
+ options.add_html = add_html
439
+ end
440
+
441
+ parser.on("--max-size=SIZE",
442
+ "Limit mail body size to SIZE",
443
+ "G/GB/M/MB/K/KB/B units are available",
444
+ "(#{format_size(options.max_size)})") do |max_size|
445
+ begin
446
+ options.max_size = parse_size(max_size)
447
+ rescue ArgumentError
448
+ raise OptionParser::InvalidArgument, max_size
449
+ end
450
+ end
451
+
452
+ parser.on("--no-limit-size",
453
+ "Don't limit mail body size",
454
+ "(#{options.max_size.nil?})") do |not_limit_size|
455
+ options.max_size = nil
456
+ end
457
+
458
+ parser.on("--max-diff-size=SIZE",
459
+ "Limit diff size to SIZE",
460
+ "G/GB/M/MB/K/KB/B units are available",
461
+ "(#{format_size(options.max_diff_size)})") do |max_size|
462
+ begin
463
+ options.max_diff_size = parse_size(max_size)
464
+ rescue ArgumentError
465
+ raise OptionParser::InvalidArgument, max_size
466
+ end
467
+ end
468
+
469
+ parser.on("--date=DATE",
470
+ "Use DATE as date of push mails (Time.parse is used)") do |date|
471
+ options.date = Time.parse(date)
472
+ end
473
+
474
+ parser.on("--git-bin-path=GIT_BIN_PATH",
475
+ "Use GIT_BIN_PATH command instead of default \"git\"") do |git_bin_path|
476
+ options.git_bin_path = git_bin_path
477
+ end
478
+
479
+ parser.on("--track-remote",
480
+ "Fetch new commits from repository's origin and send mails") do
481
+ options.track_remote = true
482
+ end
483
+ end
484
+
485
+ def add_rss_options(parser, options)
486
+ parser.separator ""
487
+ parser.separator "RSS related options:"
488
+
489
+ parser.on("--rss-path=PATH", "Use PATH as output RSS path") do |path|
490
+ options.rss_path = path
491
+ end
492
+
493
+ parser.on("--rss-uri=URI", "Use URI as output RSS URI") do |uri|
494
+ options.rss_uri = uri
495
+ end
496
+ end
497
+
498
+ def add_other_options(parser, options)
499
+ parser.separator ""
500
+ parser.separator "Other options:"
501
+
502
+ #parser.on("-IPATH", "--include=PATH", "Add PATH to load path") do |path|
503
+ # $LOAD_PATH.unshift(path)
504
+ #end
505
+ parser.on("--[no-]verbose",
506
+ "Be verbose.",
507
+ "(#{options.verbose})") do |verbose|
508
+ options.verbose = verbose
509
+ end
510
+ end
511
+ end
512
+
513
+ attr_reader :reference, :old_revision, :new_revision, :to
514
+ attr_writer :send_per_to
515
+ attr_writer :from, :add_diff, :add_html, :show_path, :send_push_mail
516
+ attr_writer :repository, :date, :git_bin_path, :track_remote
517
+ attr_accessor :from_domain, :sender, :max_size, :max_diff_size, :repository_uri
518
+ attr_accessor :rss_path, :rss_uri, :server, :port
519
+ attr_accessor :repository_browser
520
+ attr_accessor :github_base_url, :github_user, :github_repository
521
+ attr_accessor :gitlab_project_uri
522
+ attr_writer :name, :verbose
523
+ attr_accessor :sleep_per_mail
524
+
525
+ def initialize(to)
526
+ @to = to
527
+ end
528
+
529
+ def create_push_info(*args)
530
+ PushInfo.new(self, *args)
531
+ end
532
+
533
+ def create_commit_info(*args)
534
+ CommitInfo.new(self, *args)
535
+ end
536
+
537
+ def git(command, &block)
538
+ GitCommitMailer.git(git_bin_path, @repository, command, &block)
539
+ end
540
+
541
+ def get_record(revision, record)
542
+ get_records(revision, [record]).first
543
+ end
544
+
545
+ def get_records(revision, records)
546
+ GitCommitMailer.git(git_bin_path, @repository,
547
+ "log -n 1 --pretty=format:'#{records.join('%n')}%n' " +
548
+ "#{revision}").lines.collect do |line|
549
+ line.strip
550
+ end
551
+ end
552
+
553
+ def send_per_to?
554
+ @send_per_to
555
+ end
556
+
557
+ def from(info)
558
+ if @from
559
+ if /\A[^\s<]+@[^\s>]\z/ =~ @from
560
+ @from
561
+ else
562
+ "#{info.author_name} <#{@from}>"
563
+ end
564
+ else
565
+ # return "#{info.author_name}@#{@from_domain}".sub(/@\z/, '') if @from_domain
566
+ "#{info.author_name} <#{info.author_email}>"
567
+ end
568
+ end
569
+
570
+ def repository
571
+ @repository || Dir.pwd
572
+ end
573
+
574
+ def date
575
+ @date || Time.now
576
+ end
577
+
578
+ def git_bin_path
579
+ ENV['GIT_BIN_PATH'] || @git_bin_path
580
+ end
581
+
582
+ def track_remote?
583
+ @track_remote
584
+ end
585
+
586
+ def verbose?
587
+ @verbose
588
+ end
589
+
590
+ def short_new_revision
591
+ GitCommitMailer.short_revision(@new_revision)
592
+ end
593
+
594
+ def short_old_revision
595
+ GitCommitMailer.short_revision(@old_revision)
596
+ end
597
+
598
+ def origin_references
599
+ references = Hash.new("0" * 40)
600
+ git("rev-parse --symbolic-full-name --tags --remotes").lines.each do |reference|
601
+ reference.rstrip!
602
+ next if reference =~ %r!\Arefs/remotes! and reference !~ %r!\Arefs/remotes/origin!
603
+ references[reference] = git("rev-parse %s" % GitCommitMailer.shell_escape(reference)).rstrip
604
+ end
605
+ references
606
+ end
607
+
608
+ def delete_tags
609
+ git("rev-parse --symbolic --tags").lines.each do |reference|
610
+ reference.rstrip!
611
+ git("tag -d %s" % GitCommitMailer.shell_escape(reference))
612
+ end
613
+ end
614
+
615
+ def fetch
616
+ updated_references = []
617
+ old_references = origin_references
618
+ delete_tags
619
+ git("fetch --force --tags")
620
+ git("fetch --force")
621
+ new_references = origin_references
622
+
623
+ old_references.each do |reference, revision|
624
+ if revision != new_references[reference]
625
+ updated_references << [revision, new_references[reference], reference]
626
+ end
627
+ end
628
+ new_references.each do |reference, revision|
629
+ if revision != old_references[reference]#.sub(/remotes\/origin/, 'heads')
630
+ updated_references << [old_references[reference], revision, reference]
631
+ end
632
+ end
633
+ updated_references.sort do |reference_change1, reference_change2|
634
+ reference_change1.last <=> reference_change2.last
635
+ end.uniq
636
+ end
637
+
638
+ def detect_change_type
639
+ if old_revision =~ /0{40}/ and new_revision =~ /0{40}/
640
+ raise "Invalid revision hash"
641
+ elsif old_revision !~ /0{40}/ and new_revision !~ /0{40}/
642
+ :update
643
+ elsif old_revision =~ /0{40}/
644
+ :create
645
+ elsif new_revision =~ /0{40}/
646
+ :delete
647
+ else
648
+ raise "Invalid revision hash"
649
+ end
650
+ end
651
+
652
+ def detect_object_type(object_name)
653
+ git("cat-file -t #{object_name}").strip
654
+ end
655
+
656
+ def detect_revision_type(change_type)
657
+ case change_type
658
+ when :create, :update
659
+ detect_object_type(new_revision)
660
+ when :delete
661
+ detect_object_type(old_revision)
662
+ end
663
+ end
664
+
665
+ def detect_reference_type(revision_type)
666
+ if reference =~ /refs\/tags\/.*/ and revision_type == "commit"
667
+ :unannotated_tag
668
+ elsif reference =~ /refs\/tags\/.*/ and revision_type == "tag"
669
+ # change recipients
670
+ #if [ -n "$announcerecipients" ]; then
671
+ # recipients="$announcerecipients"
672
+ #fi
673
+ :annotated_tag
674
+ elsif reference =~ /refs\/(heads|remotes\/origin)\/.*/ and revision_type == "commit"
675
+ :branch
676
+ elsif reference =~ /refs\/remotes\/.*/ and revision_type == "commit"
677
+ # tracking branch
678
+ # Push-update of tracking branch.
679
+ # no email generated.
680
+ throw :no_email
681
+ else
682
+ # Anything else (is there anything else?)
683
+ raise "Unknown type of update to #@reference (#{revision_type})"
684
+ end
685
+ end
686
+
687
+ def make_push_message(reference_type, change_type)
688
+ unless [:branch, :annotated_tag, :unannotated_tag].include?(reference_type)
689
+ raise "unexpected reference_type"
690
+ end
691
+ unless [:update, :create, :delete].include?(change_type)
692
+ raise "unexpected change_type"
693
+ end
694
+
695
+ method_name = "process_#{change_type}_#{reference_type}"
696
+ __send__(method_name)
697
+ end
698
+
699
+ def collect_push_information
700
+ change_type = detect_change_type
701
+ revision_type = detect_revision_type(change_type)
702
+ reference_type = detect_reference_type(revision_type)
703
+ messsage, commits = make_push_message(reference_type, change_type)
704
+
705
+ [reference_type, change_type, messsage, commits]
706
+ end
707
+
708
+ def excluded_revisions
709
+ # refer to the long comment located at the top of this file for the
710
+ # explanation of this command.
711
+ current_reference_revision = git("rev-parse #@reference").strip
712
+ git("rev-parse --not --branches --remotes").lines.find_all do |line|
713
+ line.strip!
714
+ not line.index(current_reference_revision)
715
+ end.collect do |line|
716
+ GitCommitMailer.shell_escape(line)
717
+ end.join(' ')
718
+ end
719
+
720
+ def process_create_branch
721
+ message = "Branch (#{@reference}) is created.\n"
722
+ commits = []
723
+
724
+ commit_list = []
725
+ git("rev-list #{@new_revision} #{excluded_revisions}").lines.
726
+ reverse_each do |revision|
727
+ revision.strip!
728
+ short_revision = GitCommitMailer.short_revision(revision)
729
+ commits << revision
730
+ subject = get_record(revision, '%s')
731
+ commit_list << " via #{short_revision} #{subject}\n"
732
+ end
733
+ if commit_list.length > 0
734
+ commit_list[-1].sub!(/\A via /, ' at ')
735
+ message << commit_list.join
736
+ end
737
+
738
+ [message, commits]
739
+ end
740
+
741
+ def explain_rewind
742
+ <<EOF
743
+ This update discarded existing revisions and left the branch pointing at
744
+ a previous point in the repository history.
745
+
746
+ * -- * -- N (#{short_new_revision})
747
+ \\
748
+ O <- O <- O (#{short_old_revision})
749
+
750
+ The removed revisions are not necessarilly gone - if another reference
751
+ still refers to them they will stay in the repository.
752
+ EOF
753
+ end
754
+
755
+ def explain_rewind_and_new_commits
756
+ <<EOF
757
+ This update added new revisions after undoing existing revisions. That is
758
+ to say, the old revision is not a strict subset of the new revision. This
759
+ situation occurs when you --force push a change and generate a repository
760
+ containing something like this:
761
+
762
+ * -- * -- B <- O <- O <- O (#{short_old_revision})
763
+ \\
764
+ N -> N -> N (#{short_new_revision})
765
+
766
+ When this happens we assume that you've already had alert emails for all
767
+ of the O revisions, and so we here report only the revisions in the N
768
+ branch from the common base, B.
769
+ EOF
770
+ end
771
+
772
+ def process_backward_update
773
+ # List all of the revisions that were removed by this update, in a
774
+ # fast forward update, this list will be empty, because rev-list O
775
+ # ^N is empty. For a non fast forward, O ^N is the list of removed
776
+ # revisions
777
+ fast_forward = false
778
+ revision_found = false
779
+ commits_summary = []
780
+ git("rev-list #{@new_revision}..#{@old_revision}").lines.each do |revision|
781
+ revision_found ||= true
782
+ revision.strip!
783
+ short_revision = GitCommitMailer.short_revision(revision)
784
+ subject = get_record(revision, '%s')
785
+ commits_summary << "discards #{short_revision} #{subject}\n"
786
+ end
787
+ unless revision_found
788
+ fast_forward = true
789
+ subject = get_record(old_revision, '%s')
790
+ commits_summary << " from #{short_old_revision} #{subject}\n"
791
+ end
792
+ [fast_forward, commits_summary]
793
+ end
794
+
795
+ def process_forward_update
796
+ # List all the revisions from baserev to new_revision in a kind of
797
+ # "table-of-contents"; note this list can include revisions that
798
+ # have already had notification emails and is present to show the
799
+ # full detail of the change from rolling back the old revision to
800
+ # the base revision and then forward to the new revision
801
+ commits_summary = []
802
+ git("rev-list #{@old_revision}..#{@new_revision}").lines.each do |revision|
803
+ revision.strip!
804
+ short_revision = GitCommitMailer.short_revision(revision)
805
+
806
+ subject = get_record(revision, '%s')
807
+ commits_summary << " via #{short_revision} #{subject}\n"
808
+ end
809
+ commits_summary
810
+ end
811
+
812
+ def explain_special_case
813
+ # 1. Existing revisions were removed. In this case new_revision
814
+ # is a subset of old_revision - this is the reverse of a
815
+ # fast-forward, a rewind
816
+ # 2. New revisions were added on top of an old revision,
817
+ # this is a rewind and addition.
818
+
819
+ # (1) certainly happened, (2) possibly. When (2) hasn't
820
+ # happened, we set a flag to indicate that no log printout
821
+ # is required.
822
+
823
+ # Find the common ancestor of the old and new revisions and
824
+ # compare it with new_revision
825
+ baserev = git("merge-base #{@old_revision} #{@new_revision}").strip
826
+ rewind_only = false
827
+ if baserev == new_revision
828
+ explanation = explain_rewind
829
+ rewind_only = true
830
+ else
831
+ explanation = explain_rewind_and_new_commits
832
+ end
833
+ [rewind_only, explanation]
834
+ end
835
+
836
+ def collect_new_commits
837
+ commits = []
838
+ git("rev-list #{@old_revision}..#{@new_revision} #{excluded_revisions}").lines.
839
+ reverse_each do |revision|
840
+ commits << revision.strip
841
+ end
842
+ commits
843
+ end
844
+
845
+ def process_update_branch
846
+ message = "Branch (#{@reference}) is updated.\n"
847
+
848
+ fast_forward, backward_commits_summary = process_backward_update
849
+ forward_commits_summary = process_forward_update
850
+
851
+ commits_summary = backward_commits_summary + forward_commits_summary.reverse
852
+
853
+ unless fast_forward
854
+ rewind_only, explanation = explain_special_case
855
+ message << explanation
856
+ end
857
+
858
+ message << "\n"
859
+ message << commits_summary.join
860
+
861
+ unless rewind_only
862
+ new_commits = collect_new_commits
863
+ end
864
+ if rewind_only or new_commits.empty?
865
+ message << "\n"
866
+ message << "No new revisions were added by this update.\n"
867
+ end
868
+
869
+ [message, new_commits]
870
+ end
871
+
872
+ def process_delete_branch
873
+ "Branch (#{@reference}) is deleted.\n" +
874
+ " was #{@old_revision}\n\n" +
875
+ git("show -s --pretty=oneline #{@old_revision}")
876
+ end
877
+
878
+ def process_create_annotated_tag
879
+ "Annotated tag (#{@reference}) is created.\n" +
880
+ " at #{@new_revision} (tag)\n" +
881
+ process_annotated_tag
882
+ end
883
+
884
+ def process_update_annotated_tag
885
+ "Annotated tag (#{@reference}) is updated.\n" +
886
+ " to #{@new_revision} (tag)\n" +
887
+ " from #{@old_revision} (which is now obsolete)\n" +
888
+ process_annotated_tag
889
+ end
890
+
891
+ def process_delete_annotated_tag
892
+ "Annotated tag (#{@reference}) is deleted.\n" +
893
+ " was #{@old_revision}\n\n" +
894
+ git("show -s --pretty=oneline #{@old_revision}").sub(/^Tagger.*$/, '').
895
+ sub(/^Date.*$/, '').
896
+ sub(/\n{2,}/, "\n\n")
897
+ end
898
+
899
+ def short_log(revision_specifier)
900
+ log = git("rev-list --pretty=short #{GitCommitMailer.shell_escape(revision_specifier)}")
901
+ git("shortlog") do |git|
902
+ git.write(log)
903
+ git.close_write
904
+ return git.read
905
+ end
906
+ end
907
+
908
+ def short_log_from_previous_tag(previous_tag)
909
+ if previous_tag
910
+ # Show changes since the previous release
911
+ short_log("#{previous_tag}..#{@new_revision}")
912
+ else
913
+ # No previous tag, show all the changes since time began
914
+ short_log(@new_revision)
915
+ end
916
+ end
917
+
918
+ class NoParentCommit < Exception
919
+ end
920
+
921
+ def parent_commit(revision)
922
+ begin
923
+ git("rev-parse #{revision}^").strip
924
+ rescue
925
+ raise NoParentCommit
926
+ end
927
+ end
928
+
929
+ def previous_tag_by_revision(revision)
930
+ # If the tagged object is a commit, then we assume this is a
931
+ # release, and so we calculate which tag this tag is
932
+ # replacing
933
+ begin
934
+ git("describe --abbrev=0 #{parent_commit(revision)}").strip
935
+ rescue NoParentCommit
936
+ end
937
+ end
938
+
939
+ def annotated_tag_content
940
+ message = ''
941
+ tagger = git("for-each-ref --format='%(taggername)' #{@reference}").strip
942
+ tagged = git("for-each-ref --format='%(taggerdate:rfc2822)' #{@reference}").strip
943
+ message << " tagged by #{tagger}\n"
944
+ message << " on #{format_time(Time.rfc2822(tagged))}\n\n"
945
+
946
+ # Show the content of the tag message; this might contain a change
947
+ # log or release notes so is worth displaying.
948
+ tag_content = git("cat-file tag #{@new_revision}").split("\n")
949
+ #skips header section
950
+ tag_content.shift while not tag_content.first.empty?
951
+ #skips the empty line indicating the end of header section
952
+ tag_content.shift
953
+
954
+ message << tag_content.join("\n") + "\n"
955
+ message
956
+ end
957
+
958
+ def process_annotated_tag
959
+ message = ''
960
+ # Use git for-each-ref to pull out the individual fields from the tag
961
+ tag_object = git("for-each-ref --format='%(*objectname)' #{@reference}").strip
962
+ tag_type = git("for-each-ref --format='%(*objecttype)' #{@reference}").strip
963
+
964
+ case tag_type
965
+ when "commit"
966
+ message << " tagging #{tag_object} (#{tag_type})\n"
967
+ previous_tag = previous_tag_by_revision(@new_revision)
968
+ message << " replaces #{previous_tag}\n" if previous_tag
969
+ message << annotated_tag_content
970
+ message << short_log_from_previous_tag(previous_tag)
971
+ else
972
+ message << " tagging #{tag_object} (#{tag_type})\n"
973
+ message << " length #{git("cat-file -s #{tag_object}").strip} bytes\n"
974
+ message << annotated_tag_content
975
+ end
976
+
977
+ message
978
+ end
979
+
980
+ def process_create_unannotated_tag
981
+ raise "unexpected" unless detect_object_type(@new_revision) == "commit"
982
+
983
+ "Unannotated tag (#{@reference}) is created.\n" +
984
+ " at #{@new_revision} (commit)\n\n" +
985
+ process_unannotated_tag(@new_revision)
986
+ end
987
+
988
+ def process_update_unannotated_tag
989
+ raise "unexpected" unless detect_object_type(@new_revision) == "commit"
990
+ raise "unexpected" unless detect_object_type(@old_revision) == "commit"
991
+
992
+ "Unannotated tag (#{@reference}) is updated.\n" +
993
+ " to #{@new_revision} (commit)\n" +
994
+ " from #{@old_revision} (commit)\n\n" +
995
+ process_unannotated_tag(@new_revision)
996
+ end
997
+
998
+ def process_delete_unannotated_tag
999
+ raise "unexpected" unless detect_object_type(@old_revision) == "commit"
1000
+
1001
+ "Unannotated tag (#{@reference}) is deleted.\n" +
1002
+ " was #{@old_revision} (commit)\n\n" +
1003
+ process_unannotated_tag(@old_revision)
1004
+ end
1005
+
1006
+ def process_unannotated_tag(revision)
1007
+ git("show --no-color --root -s --pretty=short #{revision}")
1008
+ end
1009
+
1010
+ def find_branch_name_from_its_descendant_revision(revision)
1011
+ begin
1012
+ name = git("name-rev --name-only --refs refs/heads/* #{revision}").strip
1013
+ revision = parent_commit(revision)
1014
+ end until name.sub(/([~^][0-9]+)*\z/, '') == name
1015
+ name
1016
+ end
1017
+
1018
+ def traverse_merge_commit(merge_commit)
1019
+ first_grand_parent = parent_commit(merge_commit.first_parent)
1020
+
1021
+ [merge_commit.first_parent, *merge_commit.other_parents].each do |revision|
1022
+ is_traversing_first_parent = (revision == merge_commit.first_parent)
1023
+ base_revision = git("merge-base #{first_grand_parent} #{revision}").strip
1024
+ base_revisions = [@old_revision, base_revision]
1025
+ #branch_name = find_branch_name_from_its_descendant_revision(revision)
1026
+ descendant_revision = merge_commit.revision
1027
+
1028
+ until base_revisions.index(revision)
1029
+ commit_info = @commit_info_map[revision]
1030
+ if commit_info
1031
+ commit_info.reference = @reference
1032
+ else
1033
+ commit_info = create_commit_info(@reference, revision)
1034
+ index = @commit_infos.index(@commit_info_map[descendant_revision])
1035
+ @commit_infos.insert(index, commit_info)
1036
+ @commit_info_map[revision] = commit_info
1037
+ end
1038
+
1039
+ merge_message = "Merged #{merge_commit.short_revision}: #{merge_commit.subject}"
1040
+ if not is_traversing_first_parent and not commit_info.merge_status.index(merge_message)
1041
+ commit_info.merge_status << merge_message
1042
+ commit_info.merge_revisions << merge_commit.revision
1043
+ end
1044
+
1045
+ if commit_info.merge?
1046
+ traverse_merge_commit(commit_info)
1047
+ base_revision = git("merge-base #{first_grand_parent} #{commit_info.first_parent}").strip
1048
+ base_revisions << base_revision unless base_revisions.index(base_revision)
1049
+ end
1050
+ descendant_revision, revision = revision, commit_info.first_parent
1051
+ end
1052
+ end
1053
+ end
1054
+
1055
+ def post_process_infos
1056
+ # @push_info.author_name = determine_prominent_author
1057
+ commit_infos = @commit_infos.dup
1058
+ # @commit_infos may be altered and I don't know any sensible behavior of ruby
1059
+ # in such cases. Take the safety measure at the moment...
1060
+ commit_infos.reverse_each do |commit_info|
1061
+ traverse_merge_commit(commit_info) if commit_info.merge?
1062
+ end
1063
+ end
1064
+
1065
+ def determine_prominent_author
1066
+ #if @commit_infos.length > 0
1067
+ #
1068
+ #else
1069
+ # @push_info
1070
+ end
1071
+
1072
+ def reset(old_revision, new_revision, reference)
1073
+ @old_revision = old_revision
1074
+ @new_revision = new_revision
1075
+ @reference = reference
1076
+
1077
+ @push_info = nil
1078
+ @commit_infos = []
1079
+ @commit_info_map = {}
1080
+ end
1081
+
1082
+ def make_infos
1083
+ catch(:no_email) do
1084
+ @push_info = create_push_info(old_revision, new_revision, reference,
1085
+ *collect_push_information)
1086
+ if @push_info.branch_changed?
1087
+ @push_info.commits.each do |revision|
1088
+ commit_info = create_commit_info(reference, revision)
1089
+ @commit_infos << commit_info
1090
+ @commit_info_map[revision] = commit_info
1091
+ end
1092
+ end
1093
+ end
1094
+
1095
+ post_process_infos
1096
+ end
1097
+
1098
+ def make_mails
1099
+ if send_per_to?
1100
+ @push_mails = @to.collect do |to|
1101
+ make_mail(@push_info, [to])
1102
+ end
1103
+ else
1104
+ @push_mails = [make_mail(@push_info, @to)]
1105
+ end
1106
+
1107
+ @commit_mails = []
1108
+ @commit_infos.each do |info|
1109
+ if send_per_to?
1110
+ @to.each do |to|
1111
+ @commit_mails << make_mail(info, [to])
1112
+ end
1113
+ else
1114
+ @commit_mails << make_mail(info, @to)
1115
+ end
1116
+ end
1117
+ end
1118
+
1119
+ def process_reference_change(old_revision, new_revision, reference)
1120
+ reset(old_revision, new_revision, reference)
1121
+
1122
+ make_infos
1123
+ make_mails
1124
+ if rss_output_available?
1125
+ output_rss
1126
+ end
1127
+
1128
+ [@push_mails, @commit_mails]
1129
+ end
1130
+
1131
+ def send_all_mails
1132
+ if send_push_mail?
1133
+ @push_mails.each do |mail|
1134
+ send_mail(mail)
1135
+ end
1136
+ end
1137
+
1138
+ @commit_mails.each do |mail|
1139
+ send_mail(mail)
1140
+ end
1141
+ end
1142
+
1143
+ def add_diff?
1144
+ @add_diff
1145
+ end
1146
+
1147
+ def add_html?
1148
+ @add_html
1149
+ end
1150
+
1151
+ def show_path?
1152
+ @show_path
1153
+ end
1154
+
1155
+ def send_push_mail?
1156
+ @send_push_mail
1157
+ end
1158
+
1159
+ def format_time(time)
1160
+ time.strftime('%Y-%m-%d %X %z (%a, %d %b %Y)')
1161
+ end
1162
+
1163
+ private
1164
+ def send_mail(mail)
1165
+ server = @server || "localhost"
1166
+ port = @port
1167
+ from = sender || GitCommitMailer.extract_email_address_from_mail(mail)
1168
+ to = GitCommitMailer.extract_to_addresses(mail)
1169
+ GitCommitMailer.send_mail(server, port, from, to, mail)
1170
+ sleep(@sleep_per_mail)
1171
+ end
1172
+
1173
+ def output_rss
1174
+ prev_rss = nil
1175
+ begin
1176
+ if File.exist?(@rss_path)
1177
+ File.open(@rss_path) do |f|
1178
+ prev_rss = RSS::Parser.parse(f)
1179
+ end
1180
+ end
1181
+ rescue RSS::Error
1182
+ end
1183
+
1184
+ rss = make_rss(prev_rss).to_s
1185
+ File.open(@rss_path, "w") do |f|
1186
+ f.print(rss)
1187
+ end
1188
+ end
1189
+
1190
+ def rss_output_available?
1191
+ if @repository_uri and @rss_path and @rss_uri
1192
+ begin
1193
+ require 'rss'
1194
+ true
1195
+ rescue LoadError
1196
+ false
1197
+ end
1198
+ else
1199
+ false
1200
+ end
1201
+ end
1202
+
1203
+ def make_mail(info, to)
1204
+ @boundary = generate_boundary
1205
+
1206
+ encoding = "utf-8"
1207
+ bit = "8bit"
1208
+
1209
+ multipart_body_p = false
1210
+ body_text = info.format_mail_body_text
1211
+ body_html = nil
1212
+ if add_html?
1213
+ body_html = info.format_mail_body_html
1214
+ multipart_body_p = (body_text.size + body_html.size) < @max_size
1215
+ end
1216
+
1217
+ if multipart_body_p
1218
+ body = <<-EOB
1219
+ --#{@boundary}
1220
+ Content-Type: text/plain; charset=#{encoding}
1221
+ Content-Transfer-Encoding: #{bit}
1222
+
1223
+ #{body_text}
1224
+ --#{@boundary}
1225
+ Content-Type: text/html; charset=#{encoding}
1226
+ Content-Transfer-Encoding: #{bit}
1227
+
1228
+ #{body_html}
1229
+ --#{@boundary}--
1230
+ EOB
1231
+ else
1232
+ body = truncate_body(body_text, @max_size)
1233
+ end
1234
+
1235
+ header = make_header(encoding, bit, to, info, multipart_body_p)
1236
+ if header.respond_to?(:force_encoding)
1237
+ header.force_encoding("BINARY")
1238
+ body.force_encoding("BINARY")
1239
+ end
1240
+ header + "\n" + body
1241
+ end
1242
+
1243
+ def name
1244
+ return @name if @name
1245
+ repository = File.expand_path(@repository)
1246
+ loop do
1247
+ basename = File.basename(repository, ".git")
1248
+ if basename != ".git"
1249
+ return basename
1250
+ else
1251
+ repository = File.dirname(repository)
1252
+ end
1253
+ end
1254
+ end
1255
+
1256
+ def make_header(body_encoding, body_encoding_bit, to, info, multipart_body_p)
1257
+ subject = ""
1258
+ subject << "#{name}@" if name
1259
+ subject << "#{info.short_revision} "
1260
+ subject << mime_encoded_word("#{info.format_mail_subject}")
1261
+ headers = []
1262
+ headers += info.headers
1263
+ headers << "X-Mailer: #{self.class.x_mailer}"
1264
+ headers << "MIME-Version: 1.0"
1265
+ if multipart_body_p
1266
+ headers << "Content-Type: multipart/alternative;"
1267
+ headers << " boundary=#{@boundary}"
1268
+ else
1269
+ headers << "Content-Type: text/plain; charset=#{body_encoding}"
1270
+ headers << "Content-Transfer-Encoding: #{body_encoding_bit}"
1271
+ end
1272
+ headers << "From: #{from(info)}"
1273
+ headers << "To: #{to.join(', ')}"
1274
+ headers << "Subject: #{subject}"
1275
+ headers << "Date: #{info.date.rfc2822}"
1276
+ headers << "Sender: #{sender}" if sender
1277
+ headers.find_all do |header|
1278
+ /\A\s*\z/ !~ header
1279
+ end.join("\n") + "\n"
1280
+ end
1281
+
1282
+ def generate_boundary
1283
+ random_integer = Time.now.to_i * 1000 + rand(1000)
1284
+ Digest::SHA1.hexdigest(random_integer.to_s)
1285
+ end
1286
+
1287
+ def detect_project
1288
+ project = File.open("#{repository}/description").gets.strip
1289
+ # Check if the description is unchanged from it's default, and shorten it to
1290
+ # a more manageable length if it is
1291
+ if project =~ /Unnamed repository.*$/
1292
+ project = nil
1293
+ end
1294
+
1295
+ project
1296
+ end
1297
+
1298
+ def mime_encoded_word(string)
1299
+ #XXX "-MWw" didn't work in some versions of Ruby 1.9.
1300
+ # giving up to stick with UTF-8... ;)
1301
+ encoded_string = NKF.nkf("-MWj", string)
1302
+
1303
+ #XXX The actual MIME encoded-word's string representaion is US-ASCII,
1304
+ # which, in turn, can be UTF-8. In spite of this fact, in some versions
1305
+ # of Ruby 1.9, encoded_string.encoding is incorrectly set as ISO-2022-JP.
1306
+ # Fortunately, as we just said, we can just safely override them with
1307
+ # "UTF-8" to work around this bug.
1308
+ if encoded_string.respond_to?(:force_encoding)
1309
+ encoded_string.force_encoding("UTF-8")
1310
+ end
1311
+
1312
+ #XXX work around NKF's bug of gratuitously wrapping long ascii words with
1313
+ # MIME encoded-word syntax's header and footer, while not actually
1314
+ # encoding the payload as base64: just strip the header and footer out.
1315
+ encoded_string.gsub!(/\=\?EUC-JP\?B\?(.*)\?=\n /) {$1}
1316
+ encoded_string.gsub!(/(\n )*=\?US-ASCII\?Q\?(.*)\?=(\n )*/) {$2}
1317
+
1318
+ encoded_string
1319
+ end
1320
+
1321
+ def truncate_body(body, max_size)
1322
+ return body if max_size.nil?
1323
+ return body if body.size < max_size
1324
+
1325
+ truncated_body = body[0, max_size]
1326
+ formatted_size = self.class.format_size(max_size)
1327
+ truncated_message = "... truncated to #{formatted_size}\n"
1328
+ truncated_message_size = truncated_message.size
1329
+
1330
+ lf_index = truncated_body.rindex(/(?:\r|\r\n|\n)/)
1331
+ while lf_index
1332
+ if lf_index + truncated_message_size < max_size
1333
+ truncated_body[lf_index, max_size] = "\n#{truncated_message}"
1334
+ break
1335
+ else
1336
+ lf_index = truncated_body.rindex(/(?:\r|\r\n|\n)/, lf_index - 1)
1337
+ end
1338
+ end
1339
+
1340
+ truncated_body
1341
+ end
1342
+
1343
+ def make_rss(base_rss)
1344
+ RSS::Maker.make("1.0") do |maker|
1345
+ maker.encoding = "UTF-8"
1346
+
1347
+ maker.channel.about = @rss_uri
1348
+ maker.channel.title = rss_title(name || @repository_uri)
1349
+ maker.channel.link = @repository_uri
1350
+ maker.channel.description = rss_title(@name || @repository_uri)
1351
+ maker.channel.dc_date = @push_info.date
1352
+
1353
+ if base_rss
1354
+ base_rss.items.each do |item|
1355
+ item.setup_maker(maker)
1356
+ end
1357
+ end
1358
+
1359
+ @commit_infos.each do |info|
1360
+ item = maker.items.new_item
1361
+ item.title = info.rss_title
1362
+ item.description = info.summary
1363
+ item.content_encoded = info.rss_content
1364
+ item.link = "#{@repository_uri}/commit/?id=#{info.revision}"
1365
+ item.dc_date = info.date
1366
+ item.dc_creator = info.author_name
1367
+ end
1368
+
1369
+ maker.items.do_sort = true
1370
+ maker.items.max_size = 15
1371
+ end
1372
+ end
1373
+
1374
+ def rss_title(name)
1375
+ "Repository of #{name}"
1376
+ end
1377
+ end
1378
+
1379
+ require "git-commit-mailer/version"