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 +7 -0
- data/.gitignore +4 -0
- data/.travis.yml +8 -0
- data/Gemfile +4 -0
- data/README.md +47 -0
- data/Rakefile +23 -0
- data/bin/git-commit-mailer +137 -0
- data/git-commit-mailer.gemspec +26 -0
- data/lib/git-commit-mailer.rb +1379 -0
- data/lib/git-commit-mailer/commit-info.rb +291 -0
- data/lib/git-commit-mailer/file-diff.rb +356 -0
- data/lib/git-commit-mailer/html-mail-body-formatter.rb +563 -0
- data/lib/git-commit-mailer/info.rb +51 -0
- data/lib/git-commit-mailer/mail-body-formatter.rb +97 -0
- data/lib/git-commit-mailer/push-info.rb +100 -0
- data/lib/git-commit-mailer/text-mail-body-formatter.rb +87 -0
- data/lib/git-commit-mailer/version.rb +4 -0
- data/license/GPL-3.txt +674 -0
- metadata +122 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
[](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"
|