2
# Copyright (c) 2009 Martin Decky
5
# Redistribution and use in source and binary forms, with or without
6
# modification, are permitted provided that the following conditions
9
# - Redistributions of source code must retain the above copyright
10
# notice, this list of conditions and the following disclaimer.
11
# - Redistributions in binary form must reproduce the above copyright
12
# notice, this list of conditions and the following disclaimer in the
13
# documentation and/or other materials provided with the distribution.
14
# - The name of the author may not be used to endorse or promote products
15
# derived from this software without specific prior written permission.
17
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
"""Send emails for commits and repository changes."""
32
# Inspired by bzr-email plugin (copyright (c) 2005 - 2007 Canonical Ltd.,
33
# distributed under GPL), but no code is shared with the original plugin.
35
# Configuration options:
36
# - post_commit_to (destination email address for the commit emails)
37
# - post_commit_sender (source email address for the commit emails)
43
from StringIO import StringIO
45
from email.utils import parseaddr
46
from email.utils import formatdate
47
from email.utils import make_msgid
48
from email.Header import Header
49
from email.Message import Message
50
from email.mime.multipart import MIMEMultipart
51
from email.mime.text import MIMEText
53
from bzrlib import errors
54
from bzrlib import revision
55
from bzrlib import __version__ as bzrlib_version
57
from bzrlib.branch import Branch
58
from bzrlib.diff import DiffTree
60
def send_smtp(server, sender, to, subject, body):
61
"""Send SMTP message"""
63
connection = smtplib.SMTP()
66
connection.connect(server)
67
except socket.error, err:
68
raise errors.SocketConnectionError(host = server, msg = "Unable to connect to SMTP server", orig_error = err)
70
sender_user, sender_email = parseaddr(sender)
71
payload = MIMEText(body.encode("utf-8"), "plain", "utf-8")
74
msg["From"] = "%s <%s>" % (Header(unicode(sender_user)), sender_email)
75
msg["User-Agent"] = "bzr/%s" % bzrlib_version
76
msg["Date"] = formatdate(None, True)
77
msg["Message-Id"] = make_msgid("bzr")
79
msg["Subject"] = Header(subject)
82
connection.sendmail(sender, [to], msg.as_string())
84
def config_to(config):
85
"""Address the mail should go to"""
87
return config.get_user_option("post_commit_to")
89
def config_sender(config):
90
"""Address the email should be sent from"""
92
result = config.get_user_option("post_commit_sender")
94
result = config.username()
98
def merge_marker(revision):
99
if (len(revision.parent_ids) > 1):
104
def revision_sequence(branch, revision_old_id, revision_new_id):
105
"""Calculate a sequence of revisions"""
107
for revision_ac_id in branch.repository.iter_reverse_revision_history(revision_new_id):
108
if (revision_ac_id == revision_old_id):
112
def send_email(branch, revision_old_id, revision_new_id, config):
115
if (config_to(config) is not None):
117
branch.repository.lock_read()
121
for revision_ac_id in revision_sequence(branch, revision_old_id, revision_new_id):
122
revision_ac = branch.repository.get_revision(revision_ac_id)
123
revision_ac_no = branch.revision_id_to_revno(revision_ac_id)
125
committer = revision_ac.committer
126
authors = revision_ac.get_apparent_authors()
127
date = time.strftime("%Y-%m-%d %H:%M:%S %Z (%a, %d %b %Y)", time.localtime(revision_ac.timestamp))
129
if (authors != [committer]):
130
body.write("Author: %s\n" % ", ".join(authors))
132
body.write("Committer: %s\n" % committer)
133
body.write("Date: %s\n" % date)
134
body.write("New Revision: %s%s\n" % (revision_ac_no, merge_marker(revision_ac)))
135
body.write("New Id: %s\n" % revision_ac_id)
139
for parent_id in revision_ac.parent_ids:
140
body.write("Parent: %s\n" % parent_id)
142
tree_old = branch.repository.revision_tree(parent_id)
143
tree_ac = branch.repository.revision_tree(revision_ac_id)
145
delta = tree_ac.changes_from(tree_old)
147
if (len(delta.added) > 0):
148
body.write("Added:\n")
149
for item in delta.added:
150
body.write(" %s\n" % item[0])
152
if (len(delta.removed) > 0):
153
body.write("Removed:\n")
154
for item in delta.removed:
155
body.write(" %s\n" % item[0])
157
if (len(delta.renamed) > 0):
158
body.write("Renamed:\n")
159
for item in delta.renamed:
160
body.write(" %s -> %s\n" % (item[0], item[1]))
162
if (len(delta.kind_changed) > 0):
163
body.write("Changed:\n")
164
for item in delta.kind_changed:
165
body.write(" %s\n" % item[0])
167
if (len(delta.modified) > 0):
168
body.write("Modified:\n")
169
for item in delta.modified:
170
body.write(" %s\n" % item[0])
175
if (not revision_ac.message):
176
body.write("(empty)\n")
178
log = revision_ac.message.rstrip("\n\r")
179
for line in log.split("\n"):
180
body.write("%s\n" % line)
184
tree_old = branch.repository.revision_tree(revision_old_id)
185
tree_new = branch.repository.revision_tree(revision_new_id)
191
diff = DiffTree.from_trees_options(tree_old, tree_new, body, "utf8", None, "", "", None)
192
diff.show_diff(None, None)
198
revision_new_no = branch.revision_id_to_revno(revision_new_id)
200
delta = tree_new.changes_from(tree_old)
203
for item in delta.added:
204
files.append(item[0])
206
for item in delta.removed:
207
files.append(item[0])
209
for item in delta.renamed:
210
files.append(item[0])
212
for item in delta.kind_changed:
213
files.append(item[0])
215
for item in delta.modified:
216
files.append(item[0])
218
subject = "r%d - %s" % (revision_new_no, " ".join(files))
220
send_smtp("localhost", config_sender(config), config_to(config), subject, body.getvalue())
222
branch.repository.unlock()
225
def branch_post_change_hook(params):
226
"""post_change_branch_tip hook"""
228
send_email(params.branch, params.old_revid, params.new_revid, params.branch.get_config())
230
install_named_hook = getattr(Branch.hooks, "install_named_hook", None)
231
install_named_hook("post_change_branch_tip", branch_post_change_hook, "bzreml")