git-commit-mailer 1.0.0

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